forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

Compare changes

Choose any two refs to compare.

Changed files
+206 -94
api
tangled
appview
lexicons
actor
spindle
engines
nixery
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 105 105 } 106 106 } 107 107 108 - ctx := context.WithValue(r.Context(), "page", page) 108 + ctx := pagination.IntoContext(r.Context(), page) 109 109 next.ServeHTTP(w, r.WithContext(ctx)) 110 110 }) 111 111 }
+1
appview/models/profile.go
··· 19 19 Links [5]string 20 20 Stats [2]VanityStat 21 21 PinnedRepos [6]syntax.ATURI 22 + Pronouns string 22 23 } 23 24 24 25 func (p Profile) IsLinksEmpty() bool {
+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
··· 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
··· 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
··· 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
··· 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
··· 221 221 222 222 type LoginParams struct { 223 223 ReturnUrl string 224 + ErrorCode string 224 225 } 225 226 226 227 func (p *Pages) Login(w io.Writer, params LoginParams) error {
+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
··· 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
··· 83 83 </label> 84 84 <p class="text-sm text-gray-500 dark:text-gray-400">Collaborators can push to this repository.</p> 85 85 <input 86 + autocapitalize="none" 87 + autocorrect="off" 86 88 type="text" 87 89 id="add-collaborator" 88 90 name="collaborator"
+2
appview/pages/templates/spindles/fragments/addMemberModal.html
··· 30 30 </label> 31 31 <p class="text-sm text-gray-500 dark:text-gray-400">Members can register repositories and run workflows on this spindle.</p> 32 32 <input 33 + autocapitalize="none" 34 + autocorrect="off" 33 35 type="text" 34 36 id="member-did-{{ .Id }}" 35 37 name="member"
+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
··· 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
··· 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
··· 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
··· 13 13 <title>login &middot; 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
··· 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
··· 26 26 subjectIdent, err := s.idResolver.ResolveIdent(r.Context(), subject) 27 27 if err != nil { 28 28 log.Println("failed to follow, invalid did") 29 + return 29 30 } 30 31 31 32 if currentUser.Did == subjectIdent.DID.String() {
+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
··· 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
··· 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
··· 64 64 "type": "string", 65 65 "format": "at-uri" 66 66 } 67 + }, 68 + "pronouns": { 69 + "type": "string", 70 + "description": "Preferred gender pronouns.", 71 + "maxLength": 40 67 72 } 68 73 } 69 74 }
+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, "")