+1
api/tangled/actorprofile.go
+1
api/tangled/actorprofile.go
···
28
28
// pinnedRepositories: Any ATURI, it is up to appviews to validate these fields.
29
29
PinnedRepositories []string `json:"pinnedRepositories,omitempty" cborgen:"pinnedRepositories,omitempty"`
30
30
Stats []string `json:"stats,omitempty" cborgen:"stats,omitempty"`
31
+
Pronouns *string `json:"pronouns,omitempty" cborgen:"pronouns,omitempty"`
31
32
}
+7
appview/db/db.go
+7
appview/db/db.go
···
1106
1106
return err
1107
1107
})
1108
1108
1109
+
runMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1110
+
_, err := tx.Exec(`
1111
+
alter table profile add column pronouns text;
1112
+
`)
1113
+
return err
1114
+
})
1115
+
1109
1116
return &DB{
1110
1117
db,
1111
1118
logger,
+8
-5
appview/db/issues.go
+8
-5
appview/db/issues.go
···
101
101
pLower := FilterGte("row_num", page.Offset+1)
102
102
pUpper := FilterLte("row_num", page.Offset+page.Limit)
103
103
104
-
args = append(args, pLower.Arg()...)
105
-
args = append(args, pUpper.Arg()...)
106
-
pagination := " where " + pLower.Condition() + " and " + pUpper.Condition()
104
+
pageClause := ""
105
+
if page.Limit > 0 {
106
+
args = append(args, pLower.Arg()...)
107
+
args = append(args, pUpper.Arg()...)
108
+
pageClause = " where " + pLower.Condition() + " and " + pUpper.Condition()
109
+
}
107
110
108
111
query := fmt.Sprintf(
109
112
`
···
128
131
%s
129
132
`,
130
133
whereClause,
131
-
pagination,
134
+
pageClause,
132
135
)
133
136
134
137
rows, err := e.Query(query, args...)
···
244
247
}
245
248
246
249
func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) {
247
-
return GetIssuesPaginated(e, pagination.FirstPage(), filters...)
250
+
return GetIssuesPaginated(e, pagination.Page{}, filters...)
248
251
}
249
252
250
253
func AddIssueComment(e Execer, c models.IssueComment) (int64, error) {
+7
-4
appview/db/notifications.go
+7
-4
appview/db/notifications.go
···
60
60
whereClause += " AND " + condition
61
61
}
62
62
}
63
+
pageClause := ""
64
+
if page.Limit > 0 {
65
+
pageClause = " limit ? offset ? "
66
+
args = append(args, page.Limit, page.Offset)
67
+
}
63
68
64
69
query := fmt.Sprintf(`
65
70
select id, recipient_did, actor_did, type, entity_type, entity_id, read, created, repo_id, issue_id, pull_id
66
71
from notifications
67
72
%s
68
73
order by created desc
69
-
limit ? offset ?
70
-
`, whereClause)
71
-
72
-
args = append(args, page.Limit, page.Offset)
74
+
%s
75
+
`, whereClause, pageClause)
73
76
74
77
rows, err := e.QueryContext(context.Background(), query, args...)
75
78
if err != nil {
+26
-6
appview/db/profile.go
+26
-6
appview/db/profile.go
···
129
129
did,
130
130
description,
131
131
include_bluesky,
132
-
location
132
+
location,
133
+
pronouns
133
134
)
134
-
values (?, ?, ?, ?)`,
135
+
values (?, ?, ?, ?, ?)`,
135
136
profile.Did,
136
137
profile.Description,
137
138
includeBskyValue,
138
139
profile.Location,
140
+
profile.Pronouns,
139
141
)
140
142
141
143
if err != nil {
···
216
218
did,
217
219
description,
218
220
include_bluesky,
219
-
location
221
+
location,
222
+
pronouns
220
223
from
221
224
profile
222
225
%s`,
···
231
234
for rows.Next() {
232
235
var profile models.Profile
233
236
var includeBluesky int
237
+
var pronouns sql.Null[string]
234
238
235
-
err = rows.Scan(&profile.ID, &profile.Did, &profile.Description, &includeBluesky, &profile.Location)
239
+
err = rows.Scan(&profile.ID, &profile.Did, &profile.Description, &includeBluesky, &profile.Location, &pronouns)
236
240
if err != nil {
237
241
return nil, err
238
242
}
239
243
240
244
if includeBluesky != 0 {
241
245
profile.IncludeBluesky = true
246
+
}
247
+
248
+
if pronouns.Valid {
249
+
profile.Pronouns = pronouns.V
242
250
}
243
251
244
252
profileMap[profile.Did] = &profile
···
302
310
303
311
func GetProfile(e Execer, did string) (*models.Profile, error) {
304
312
var profile models.Profile
313
+
var pronouns sql.Null[string]
314
+
305
315
profile.Did = did
306
316
307
317
includeBluesky := 0
318
+
308
319
err := e.QueryRow(
309
-
`select description, include_bluesky, location from profile where did = ?`,
320
+
`select description, include_bluesky, location, pronouns from profile where did = ?`,
310
321
did,
311
-
).Scan(&profile.Description, &includeBluesky, &profile.Location)
322
+
).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns)
312
323
if err == sql.ErrNoRows {
313
324
profile := models.Profile{}
314
325
profile.Did = did
···
321
332
322
333
if includeBluesky != 0 {
323
334
profile.IncludeBluesky = true
335
+
}
336
+
337
+
if pronouns.Valid {
338
+
profile.Pronouns = pronouns.V
324
339
}
325
340
326
341
rows, err := e.Query(`select link from profile_links where did = ?`, did)
···
412
427
// ensure description is not too long
413
428
if len(profile.Location) > 40 {
414
429
return fmt.Errorf("Entered location is too long.")
430
+
}
431
+
432
+
// ensure pronouns are not too long
433
+
if len(profile.Pronouns) > 40 {
434
+
return fmt.Errorf("Entered pronouns are too long.")
415
435
}
416
436
417
437
// ensure links are in order
+6
appview/ingester.go
+6
appview/ingester.go
···
291
291
292
292
includeBluesky := record.Bluesky
293
293
294
+
pronouns := ""
295
+
if record.Pronouns != nil {
296
+
pronouns = *record.Pronouns
297
+
}
298
+
294
299
location := ""
295
300
if record.Location != nil {
296
301
location = *record.Location
···
325
330
Links: links,
326
331
Stats: stats,
327
332
PinnedRepos: pinned,
333
+
Pronouns: pronouns,
328
334
}
329
335
330
336
ddb, ok := i.Db.Execer.(*db.DB)
+1
-5
appview/issues/issues.go
+1
-5
appview/issues/issues.go
···
770
770
isOpen = true
771
771
}
772
772
773
-
page, ok := r.Context().Value("page").(pagination.Page)
774
-
if !ok {
775
-
l.Error("failed to get page")
776
-
page = pagination.FirstPage()
777
-
}
773
+
page := pagination.FromContext(r.Context())
778
774
779
775
user := rp.oauth.GetUser(r)
780
776
f, err := rp.repoResolver.Resolve(r)
+1
-1
appview/middleware/middleware.go
+1
-1
appview/middleware/middleware.go
+1
appview/models/profile.go
+1
appview/models/profile.go
+1
-5
appview/notifications/notifications.go
+1
-5
appview/notifications/notifications.go
···
49
49
l := n.logger.With("handler", "notificationsPage")
50
50
user := n.oauth.GetUser(r)
51
51
52
-
page, ok := r.Context().Value("page").(pagination.Page)
53
-
if !ok {
54
-
l.Error("failed to get page")
55
-
page = pagination.FirstPage()
56
-
}
52
+
page := pagination.FromContext(r.Context())
57
53
58
54
total, err := db.CountNotifications(
59
55
n.db,
+42
-50
appview/notify/merged_notifier.go
+42
-50
appview/notify/merged_notifier.go
···
2
2
3
3
import (
4
4
"context"
5
+
"reflect"
6
+
"sync"
5
7
6
8
"tangled.org/core/appview/models"
7
9
)
···
16
18
17
19
var _ Notifier = &mergedNotifier{}
18
20
19
-
func (m *mergedNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
20
-
for _, notifier := range m.notifiers {
21
-
notifier.NewRepo(ctx, repo)
21
+
// fanout calls the same method on all notifiers concurrently
22
+
func (m *mergedNotifier) fanout(method string, args ...any) {
23
+
var wg sync.WaitGroup
24
+
for _, n := range m.notifiers {
25
+
wg.Add(1)
26
+
go func(notifier Notifier) {
27
+
defer wg.Done()
28
+
v := reflect.ValueOf(notifier).MethodByName(method)
29
+
in := make([]reflect.Value, len(args))
30
+
for i, arg := range args {
31
+
in[i] = reflect.ValueOf(arg)
32
+
}
33
+
v.Call(in)
34
+
}(n)
22
35
}
36
+
wg.Wait()
37
+
}
38
+
39
+
func (m *mergedNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
40
+
m.fanout("NewRepo", ctx, repo)
23
41
}
24
42
25
43
func (m *mergedNotifier) NewStar(ctx context.Context, star *models.Star) {
26
-
for _, notifier := range m.notifiers {
27
-
notifier.NewStar(ctx, star)
28
-
}
44
+
m.fanout("NewStar", ctx, star)
29
45
}
46
+
30
47
func (m *mergedNotifier) DeleteStar(ctx context.Context, star *models.Star) {
31
-
for _, notifier := range m.notifiers {
32
-
notifier.DeleteStar(ctx, star)
33
-
}
48
+
m.fanout("DeleteStar", ctx, star)
34
49
}
35
50
36
51
func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
37
-
for _, notifier := range m.notifiers {
38
-
notifier.NewIssue(ctx, issue)
39
-
}
52
+
m.fanout("NewIssue", ctx, issue)
40
53
}
54
+
41
55
func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {
42
-
for _, notifier := range m.notifiers {
43
-
notifier.NewIssueComment(ctx, comment)
44
-
}
56
+
m.fanout("NewIssueComment", ctx, comment)
45
57
}
46
58
47
59
func (m *mergedNotifier) NewIssueClosed(ctx context.Context, issue *models.Issue) {
48
-
for _, notifier := range m.notifiers {
49
-
notifier.NewIssueClosed(ctx, issue)
50
-
}
60
+
m.fanout("NewIssueClosed", ctx, issue)
51
61
}
52
62
53
63
func (m *mergedNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
54
-
for _, notifier := range m.notifiers {
55
-
notifier.NewFollow(ctx, follow)
56
-
}
64
+
m.fanout("NewFollow", ctx, follow)
57
65
}
66
+
58
67
func (m *mergedNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {
59
-
for _, notifier := range m.notifiers {
60
-
notifier.DeleteFollow(ctx, follow)
61
-
}
68
+
m.fanout("DeleteFollow", ctx, follow)
62
69
}
63
70
64
71
func (m *mergedNotifier) NewPull(ctx context.Context, pull *models.Pull) {
65
-
for _, notifier := range m.notifiers {
66
-
notifier.NewPull(ctx, pull)
67
-
}
72
+
m.fanout("NewPull", ctx, pull)
68
73
}
74
+
69
75
func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment) {
70
-
for _, notifier := range m.notifiers {
71
-
notifier.NewPullComment(ctx, comment)
72
-
}
76
+
m.fanout("NewPullComment", ctx, comment)
73
77
}
74
78
75
79
func (m *mergedNotifier) NewPullMerged(ctx context.Context, pull *models.Pull) {
76
-
for _, notifier := range m.notifiers {
77
-
notifier.NewPullMerged(ctx, pull)
78
-
}
80
+
m.fanout("NewPullMerged", ctx, pull)
79
81
}
80
82
81
83
func (m *mergedNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
82
-
for _, notifier := range m.notifiers {
83
-
notifier.NewPullClosed(ctx, pull)
84
-
}
84
+
m.fanout("NewPullClosed", ctx, pull)
85
85
}
86
86
87
87
func (m *mergedNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {
88
-
for _, notifier := range m.notifiers {
89
-
notifier.UpdateProfile(ctx, profile)
90
-
}
88
+
m.fanout("UpdateProfile", ctx, profile)
91
89
}
92
90
93
-
func (m *mergedNotifier) NewString(ctx context.Context, string *models.String) {
94
-
for _, notifier := range m.notifiers {
95
-
notifier.NewString(ctx, string)
96
-
}
91
+
func (m *mergedNotifier) NewString(ctx context.Context, s *models.String) {
92
+
m.fanout("NewString", ctx, s)
97
93
}
98
94
99
-
func (m *mergedNotifier) EditString(ctx context.Context, string *models.String) {
100
-
for _, notifier := range m.notifiers {
101
-
notifier.EditString(ctx, string)
102
-
}
95
+
func (m *mergedNotifier) EditString(ctx context.Context, s *models.String) {
96
+
m.fanout("EditString", ctx, s)
103
97
}
104
98
105
99
func (m *mergedNotifier) DeleteString(ctx context.Context, did, rkey string) {
106
-
for _, notifier := range m.notifiers {
107
-
notifier.DeleteString(ctx, did, rkey)
108
-
}
100
+
m.fanout("DeleteString", ctx, did, rkey)
109
101
}
+13
-2
appview/oauth/handler.go
+13
-2
appview/oauth/handler.go
···
4
4
"bytes"
5
5
"context"
6
6
"encoding/json"
7
+
"errors"
7
8
"fmt"
8
9
"net/http"
9
10
"slices"
10
11
"time"
11
12
13
+
"github.com/bluesky-social/indigo/atproto/auth/oauth"
12
14
"github.com/go-chi/chi/v5"
13
15
"github.com/lestrrat-go/jwx/v2/jwk"
14
16
"github.com/posthog/posthog-go"
···
58
60
59
61
func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) {
60
62
ctx := r.Context()
63
+
l := o.Logger.With("query", r.URL.Query())
61
64
62
65
sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query())
63
66
if err != nil {
64
-
http.Error(w, err.Error(), http.StatusInternalServerError)
67
+
var callbackErr *oauth.AuthRequestCallbackError
68
+
if errors.As(err, &callbackErr) {
69
+
l.Debug("callback error", "err", callbackErr)
70
+
http.Redirect(w, r, fmt.Sprintf("/login?error=%s", callbackErr.ErrorCode), http.StatusFound)
71
+
return
72
+
}
73
+
l.Error("failed to process callback", "err", err)
74
+
http.Redirect(w, r, "/login?error=oauth", http.StatusFound)
65
75
return
66
76
}
67
77
68
78
if err := o.SaveSession(w, r, sessData); err != nil {
69
-
http.Error(w, err.Error(), http.StatusInternalServerError)
79
+
l.Error("failed to save session", "data", sessData, "err", err)
80
+
http.Redirect(w, r, "/login?error=session", http.StatusFound)
70
81
return
71
82
}
72
83
+4
-1
appview/oauth/oauth.go
+4
-1
appview/oauth/oauth.go
···
58
58
59
59
sessStore := sessions.NewCookieStore([]byte(config.Core.CookieSecret))
60
60
61
+
clientApp := oauth.NewClientApp(&oauthConfig, authStore)
62
+
clientApp.Dir = res.Directory()
63
+
61
64
return &OAuth{
62
-
ClientApp: oauth.NewClientApp(&oauthConfig, authStore),
65
+
ClientApp: clientApp,
63
66
Config: config,
64
67
SessStore: sessStore,
65
68
JwksUri: jwksUri,
+3
-2
appview/pages/funcmap.go
+3
-2
appview/pages/funcmap.go
···
297
297
},
298
298
299
299
"normalizeForHtmlId": func(s string) string {
300
-
// TODO: extend this to handle other cases?
301
-
return strings.ReplaceAll(s, ":", "_")
300
+
normalized := strings.ReplaceAll(s, ":", "_")
301
+
normalized = strings.ReplaceAll(normalized, ".", "_")
302
+
return normalized
302
303
},
303
304
"sshFingerprint": func(pubKey string) string {
304
305
fp, err := crypto.SSHFingerprint(pubKey)
+1
appview/pages/pages.go
+1
appview/pages/pages.go
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
···
34
34
35
35
{{ define "editIssueComment" }}
36
36
<a
37
-
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group"
37
+
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer"
38
38
hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/edit"
39
39
hx-swap="outerHTML"
40
40
hx-target="#comment-body-{{.Comment.Id}}">
···
44
44
45
45
{{ define "deleteIssueComment" }}
46
46
<a
47
-
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group"
47
+
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer"
48
48
hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/"
49
49
hx-confirm="Are you sure you want to delete your comment?"
50
50
hx-swap="outerHTML"
+2
-2
appview/pages/templates/repo/issues/issue.html
+2
-2
appview/pages/templates/repo/issues/issue.html
···
84
84
85
85
{{ define "editIssue" }}
86
86
<a
87
-
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group"
87
+
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer"
88
88
hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/edit"
89
89
hx-swap="innerHTML"
90
90
hx-target="#issue-{{.Issue.IssueId}}">
···
94
94
95
95
{{ define "deleteIssue" }}
96
96
<a
97
-
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group"
97
+
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer"
98
98
hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/"
99
99
hx-confirm="Are you sure you want to delete your issue?"
100
100
hx-swap="none">
+2
appview/pages/templates/repo/settings/access.html
+2
appview/pages/templates/repo/settings/access.html
+2
appview/pages/templates/spindles/fragments/addMemberModal.html
+2
appview/pages/templates/spindles/fragments/addMemberModal.html
+1
-1
appview/pages/templates/strings/string.html
+1
-1
appview/pages/templates/strings/string.html
···
47
47
</span>
48
48
</section>
49
49
<section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white">
50
-
<div class="flex justify-between items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
50
+
<div class="flex flex-col md:flex-row md:justify-between md:items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
51
51
<span>
52
52
{{ .String.Filename }}
53
53
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
+11
appview/pages/templates/user/fragments/editBio.html
+11
appview/pages/templates/user/fragments/editBio.html
···
20
20
</div>
21
21
22
22
<div class="flex flex-col gap-1">
23
+
<label class="m-0 p-0" for="pronouns">pronouns</label>
24
+
<div class="flex items-center gap-2 w-full">
25
+
{{ $pronouns := "" }}
26
+
{{ if and .Profile .Profile.Pronouns }}
27
+
{{ $pronouns = .Profile.Pronouns }}
28
+
{{ end }}
29
+
<input type="text" class="py-1 px-1 w-full" name="pronouns" value="{{ $pronouns }}">
30
+
</div>
31
+
</div>
32
+
33
+
<div class="flex flex-col gap-1">
23
34
<label class="m-0 p-0" for="location">location</label>
24
35
<div class="flex items-center gap-2 w-full">
25
36
{{ $location := "" }}
+1
-1
appview/pages/templates/user/fragments/followCard.html
+1
-1
appview/pages/templates/user/fragments/followCard.html
···
3
3
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 rounded-sm">
4
4
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4">
5
5
<div class="flex-shrink-0 max-h-full w-24 h-24">
6
-
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" />
6
+
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" alt="{{ $userIdent }}" />
7
7
</div>
8
8
9
9
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full">
+5
appview/pages/templates/user/fragments/profileCard.html
+5
appview/pages/templates/user/fragments/profileCard.html
···
12
12
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
13
13
{{ $userIdent }}
14
14
</p>
15
+
{{ with .Profile }}
16
+
{{ if .Pronouns }}
17
+
<p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p>
18
+
{{ end }}
19
+
{{ end }}
15
20
<a href="/{{ $userIdent }}/feed.atom">{{ i "rss" "size-4" }}</a>
16
21
</div>
17
22
+23
-2
appview/pages/templates/user/login.html
+23
-2
appview/pages/templates/user/login.html
···
13
13
<title>login · tangled</title>
14
14
</head>
15
15
<body class="flex items-center justify-center min-h-screen">
16
-
<main class="max-w-md px-6 -mt-4">
16
+
<main class="max-w-md px-7 mt-4">
17
17
<h1 class="flex place-content-center text-3xl font-semibold italic dark:text-white" >
18
18
{{ template "fragments/logotype" }}
19
19
</h1>
···
21
21
tightly-knit social coding.
22
22
</h2>
23
23
<form
24
-
class="mt-4 max-w-sm mx-auto"
24
+
class="mt-4"
25
25
hx-post="/login"
26
26
hx-swap="none"
27
27
hx-disabled-elt="#login-button"
···
29
29
<div class="flex flex-col">
30
30
<label for="handle">handle</label>
31
31
<input
32
+
autocapitalize="none"
33
+
autocorrect="off"
34
+
autocomplete="username"
32
35
type="text"
33
36
id="handle"
34
37
name="handle"
···
53
56
<span>login</span>
54
57
</button>
55
58
</form>
59
+
{{ if .ErrorCode }}
60
+
<div class="flex gap-2 my-2 bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-3 py-2 text-red-500 dark:text-red-300">
61
+
<span class="py-1">{{ i "circle-alert" "w-4 h-4" }}</span>
62
+
<div>
63
+
<h5 class="font-medium">Login error</h5>
64
+
<p class="text-sm">
65
+
{{ if eq .ErrorCode "access_denied" }}
66
+
You have not authorized the app.
67
+
{{ else if eq .ErrorCode "session" }}
68
+
Server failed to create user session.
69
+
{{ else }}
70
+
Internal Server error.
71
+
{{ end }}
72
+
Please try again.
73
+
</p>
74
+
</div>
75
+
</div>
76
+
{{ end }}
56
77
<p class="text-sm text-gray-500">
57
78
Don't have an account? <a href="/signup" class="underline">Create an account</a> on Tangled now!
58
79
</p>
+23
appview/pagination/page.go
+23
appview/pagination/page.go
···
1
1
package pagination
2
2
3
+
import "context"
4
+
3
5
type Page struct {
4
6
Offset int // where to start from
5
7
Limit int // number of items in a page
···
10
12
Offset: 0,
11
13
Limit: 30,
12
14
}
15
+
}
16
+
17
+
type ctxKey struct{}
18
+
19
+
func IntoContext(ctx context.Context, page Page) context.Context {
20
+
return context.WithValue(ctx, ctxKey{}, page)
21
+
}
22
+
23
+
func FromContext(ctx context.Context) Page {
24
+
if ctx == nil {
25
+
return FirstPage()
26
+
}
27
+
v := ctx.Value(ctxKey{})
28
+
if v == nil {
29
+
return FirstPage()
30
+
}
31
+
page, ok := v.(Page)
32
+
if !ok {
33
+
return FirstPage()
34
+
}
35
+
return page
13
36
}
14
37
15
38
func (p Page) Previous() Page {
+1
appview/state/follow.go
+1
appview/state/follow.go
+1
-4
appview/state/gfi.go
+1
-4
appview/state/gfi.go
···
18
18
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
19
19
user := s.oauth.GetUser(r)
20
20
21
-
page, ok := r.Context().Value("page").(pagination.Page)
22
-
if !ok {
23
-
page = pagination.FirstPage()
24
-
}
21
+
page := pagination.FromContext(r.Context())
25
22
26
23
goodFirstIssueLabel := fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "good-first-issue")
27
24
+2
appview/state/login.go
+2
appview/state/login.go
···
14
14
switch r.Method {
15
15
case http.MethodGet:
16
16
returnURL := r.URL.Query().Get("return_url")
17
+
errorCode := r.URL.Query().Get("error")
17
18
s.pages.Login(w, pages.LoginParams{
18
19
ReturnUrl: returnURL,
20
+
ErrorCode: errorCode,
19
21
})
20
22
case http.MethodPost:
21
23
handle := r.FormValue("handle")
+2
appview/state/profile.go
+2
appview/state/profile.go
···
538
538
profile.Description = r.FormValue("description")
539
539
profile.IncludeBluesky = r.FormValue("includeBluesky") == "on"
540
540
profile.Location = r.FormValue("location")
541
+
profile.Pronouns = r.FormValue("pronouns")
541
542
542
543
var links [5]string
543
544
for i := range 5 {
···
652
653
Location: &profile.Location,
653
654
PinnedRepositories: pinnedRepoStrings,
654
655
Stats: vanityStats[:],
656
+
Pronouns: &profile.Pronouns,
655
657
}},
656
658
SwapRecord: cid,
657
659
})
+5
lexicons/actor/profile.json
+5
lexicons/actor/profile.json
+1
-1
spindle/engines/nixery/engine.go
+1
-1
spindle/engines/nixery/engine.go
···
222
222
},
223
223
ReadonlyRootfs: false,
224
224
CapDrop: []string{"ALL"},
225
-
CapAdd: []string{"CAP_DAC_OVERRIDE"},
225
+
CapAdd: []string{"CAP_DAC_OVERRIDE", "CAP_CHOWN", "CAP_FOWNER", "CAP_SETUID", "CAP_SETGID"},
226
226
SecurityOpt: []string{"no-new-privileges"},
227
227
ExtraHosts: []string{"host.docker.internal:host-gateway"},
228
228
}, nil, nil, "")