Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

appview/{pages,state/comment}: redirect to correct location

Introduce aturi->url maker in `pages.Pages`. This can be used throughout
the codebase later.

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
date (Jun 16, 2026, 11:29 PM +0900) commit 9b5b428f parent 5c97f1cc change-id nqupmvvn
+119 -3
+113
appview/pages/url.go
··· 1 + package pages 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "fmt" 7 + "path" 8 + "strconv" 9 + 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "tangled.org/core/api/tangled" 12 + "tangled.org/core/appview/db" 13 + "tangled.org/core/appview/models" 14 + "tangled.org/core/orm" 15 + ) 16 + 17 + func (p *Pages) MakeCommentUrl(ctx context.Context, uri syntax.ATURI) (string, error) { 18 + comment, err := db.GetComment(p.db, orm.FilterEq("at_uri", uri)) 19 + if err != nil { 20 + return "", fmt.Errorf("failed to get comment: %w", err) 21 + } 22 + subjectUri := syntax.ATURI(comment.Subject.Uri) 23 + switch subjectUri.Collection() { 24 + case tangled.RepoIssueNSID: 25 + issueUrl, err := p.MakeIssueUrl(ctx, subjectUri) 26 + if err != nil { 27 + return "", fmt.Errorf("failed to make issue url: %w", err) 28 + } 29 + return issueUrl + fmt.Sprintf("#comment-%s", comment.Rkey), nil 30 + case tangled.RepoPullNSID: 31 + if comment.PullRoundIdx == nil { 32 + return "", fmt.Errorf("comment.pullRoundIdx is missing") 33 + } 34 + pullUrl, err := p.MakePullUrl(ctx, subjectUri, *comment.PullRoundIdx) 35 + if err != nil { 36 + return "", fmt.Errorf("failed to make pull url: %w", err) 37 + } 38 + return pullUrl + fmt.Sprintf("#comment-%s", comment.Rkey), nil 39 + case tangled.StringNSID: 40 + stringUrl, err := p.MakeStringUrl(ctx, subjectUri) 41 + if err != nil { 42 + return "", fmt.Errorf("failed to make string url: %w", err) 43 + } 44 + return stringUrl + fmt.Sprintf("#comment-%s", comment.Rkey), nil 45 + default: 46 + return "", fmt.Errorf("unknown subject collection '%s'", subjectUri.Collection()) 47 + } 48 + } 49 + 50 + func (p *Pages) MakeIssueUrl(ctx context.Context, uri syntax.ATURI) (string, error) { 51 + issue, err := func(uri syntax.ATURI) (*models.Issue, error) { 52 + issues, err := db.GetIssues(p.db, orm.FilterEq("at_uri", uri)) 53 + if err != nil { 54 + return nil, err 55 + } 56 + if len(issues) != 1 { 57 + return nil, sql.ErrNoRows 58 + } 59 + return &issues[0], nil 60 + }(uri) 61 + if err != nil { 62 + return "", fmt.Errorf("failed to get issue: %w", err) 63 + } 64 + repoUrl, err := p.makeRepoUrlInner(ctx, issue.Repo) 65 + if err != nil { 66 + return "", fmt.Errorf("failed to make repo url: %w", err) 67 + } 68 + return path.Join(repoUrl, "issues", strconv.FormatInt(issue.Id, 10)), nil 69 + } 70 + 71 + func (p *Pages) MakePullUrl(ctx context.Context, uri syntax.ATURI, roundIdx int) (string, error) { 72 + pull, err := db.GetPull(p.db, orm.FilterEq("at_uri", uri)) 73 + if err != nil { 74 + return "", fmt.Errorf("failed to get pull: %w", err) 75 + } 76 + repoUrl, err := p.makeRepoUrlInner(ctx, pull.Repo) 77 + if err != nil { 78 + return "", fmt.Errorf("failed to make repo url: %w", err) 79 + } 80 + return path.Join(repoUrl, "pulls", strconv.Itoa(pull.ID), "rounds", strconv.Itoa(roundIdx)), nil 81 + } 82 + 83 + func (p *Pages) makeRepoUrlInner(ctx context.Context, repo *models.Repo) (string, error) { 84 + owner := repo.Did 85 + ownerIdentity, err := p.resolver.Directory().LookupDID(ctx, syntax.DID(repo.Did)) 86 + if err == nil && !ownerIdentity.Handle.IsInvalidHandle() { 87 + owner = ownerIdentity.Handle.String() 88 + } 89 + return path.Join("/", owner, repo.Slug()), nil 90 + } 91 + 92 + func (p *Pages) MakeStringUrl(ctx context.Context, uri syntax.ATURI) (string, error) { 93 + string_, err := func(uri syntax.ATURI) (*models.String, error) { 94 + strings, err := db.GetStrings(p.db, 1, orm.FilterEq("at_uri", uri)) 95 + if err != nil { 96 + return nil, err 97 + } 98 + if len(strings) != 1 { 99 + return nil, sql.ErrNoRows 100 + } 101 + return &strings[0], nil 102 + }(uri) 103 + if err != nil { 104 + return "", fmt.Errorf("failed to get string: %w", err) 105 + } 106 + 107 + owner := string_.Did.String() 108 + ownerIdentity, err := p.resolver.Directory().LookupDID(ctx, string_.Did) 109 + if err == nil && !ownerIdentity.Handle.IsInvalidHandle() { 110 + owner = ownerIdentity.Handle.String() 111 + } 112 + return path.Join("/strings", owner, string_.Rkey), nil 113 + }
+6 -3
appview/state/comment.go
··· 295 295 296 296 s.notifier.NewComment(ctx, &comment, mentions) 297 297 298 - // TODO: return comment or reply-comment fragment 299 - // onattach, htmx-callback to focus on comment. 300 - s.pages.HxRefresh(w) 298 + target, err := s.pages.MakeCommentUrl(ctx, comment.AtUri()) 299 + if err != nil { 300 + s.pages.HxRefresh(w) 301 + } 302 + 303 + s.pages.HxLocation(w, target) 301 304 } 302 305 303 306 func (s *State) EditComment(w http.ResponseWriter, r *http.Request) {