Approval, push notice, file, folder and wiki proposal links use the path scheme, with people written by member name when they have one.
- commit
- f35f8dff89e66aebe8a44ec0fd7c41a71485a6a1
- parent
- 196be3813d45b7d852cc18e69856e0ca47cd76c3
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 18 | +++++++------- |
| M | internal/ | 3 | +- |
| M | internal/ | 10 | ++++---- |
| M | internal/ | 2 | +- |
| M | internal/ | 23 | +++++++++++++---- |
| M | internal/ | 16 | ++++++------ |
| M | internal/ | 6 | ++-- |
| M | internal/ | 2 | +- |
| M | internal/ | 2 | +- |
| A | internal/ | 36 | ++++++++++++++++++++++++++ |
| A | internal/ | 41 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 2 | +- |
| M | internal/ | 6 | ++-- |
| M | internal/ | 3 | +- |
| M | internal/ | 12 | ++++---- |
| M | internal/ | 2 | +- |
@@ -153,19 +153,20 @@ func approvalAsked(e event.Event) []string {
153153 return pushRecipients(e)154154 }155155 156-func approvalItemFrom(e event.Event, kind string, base string) approvalItem {156+func (t *Tenant) approvalItemFrom(ctx context.Context, e event.Event, kind string) approvalItem {157157 item := approvalItem{ID: e.ID, Kind: e.Kind, Type: kind, Asker: e.PubKey, Asked: approvalAsked(e), Subject: strings.TrimSpace(event.Tag(e, "subject")), Content: e.Content, CreatedAt: e.CreatedAt, Expires: event.Expiration(e), State: "open", Room: event.Tag(e, "h"), Event: e}158158 item.Options, item.Selection, _ = nativeInteraction(e)159159 item.Freeform = event.Tag(e, "freeform") == "true"160160 item.Interaction = event.Tag(e, "interaction")161- item.About = approvalAboutFrom(e, base)161+ item.About = t.approvalAboutFrom(ctx, e)162162 return item163163 }164164 165165 // approvalAboutFrom resolves the request's a or e reference to a page. A166166 // repository coordinate opens the repository, or its issue or pull request167167 // when the root is one; any other event opens the event page.168-func approvalAboutFrom(e event.Event, base string) *approvalAbout {168+func (t *Tenant) approvalAboutFrom(ctx context.Context, e event.Event) *approvalAbout {169+ base := strings.TrimRight(t.publicURL, "/")169170 coordinate := ""170171 for _, name := range []string{"a", "A"} {171172 if value := event.Tag(e, name); value != "" {@@ -185,9 +186,9 @@ func approvalAboutFrom(e event.Event, base string) *approvalAbout {
185186 }186187 about := &approvalAbout{Coordinate: coordinate, Event: referenced, Kind: kind}187188 if owner, identifier, ok := repoCoordinate(coordinate); ok {188- about.URL = base + "/repo?owner=" + owner + "&repo=" + identifier + "&view=home"189- if view := map[string]string{"1621": "issue", "1618": "pr"}[kind]; view != "" && referenced != "" {190- about.URL = base + "/repo?owner=" + owner + "&repo=" + identifier + "&view=" + view + "&id=" + referenced189+ about.URL = t.repoPageURL(ctx, owner, identifier)190+ if section := map[string]string{"1621": "issues", "1618": "prs"}[kind]; section != "" && referenced != "" {191+ about.URL = t.repoItemURL(ctx, owner, identifier, section, referenced)191192 }192193 } else if referenced != "" {193194 about.URL = base + "/e/" + referenced@@ -429,7 +430,6 @@ func approvalSettle(item *approvalItem, rows []event.Event, now int64) {
429430 // what lapsed until maintenance removes them.430431 func (t *Tenant) approvalsFor(ctx context.Context, pubkey string, before *storage.EventCursor, limit int) ([]approvalItem, bool, error) {431432 now := time.Now().Unix()432- base := strings.TrimRight(t.publicURL, "/")433433 page, err := t.store.Query(ctx, event.Filter{Kinds: approvalKinds, Tags: map[string][]string{"p": {pubkey}}}, storage.QueryOptions{Access: storage.Access{PubKeys: []string{pubkey}}, Limit: limit, Before: before})434434 if err != nil {435435 return nil, false, err@@ -440,7 +440,7 @@ func (t *Tenant) approvalsFor(ctx context.Context, pubkey string, before *storag
440440 if !ok || row.PubKey == pubkey {441441 continue442442 }443- items = append(items, approvalItemFrom(row, kind, base))443+ items = append(items, t.approvalItemFrom(ctx, row, kind))444444 }445445 answers, err := t.approvalAnswers(ctx, items, now)446446 if err != nil {@@ -517,7 +517,7 @@ func (t *Tenant) approvalNotices(e event.Event, kind string) []pushNotice {
517517 if native && !containsString(event.TagValues(e, "mention"), recipient) {518518 continue519519 }520- notices = append(notices, pushNotice{recipient: recipient, category: pushApprovals, body: body, url: base + "/approvals?id=" + e.ID, actions: actions})520+ notices = append(notices, pushNotice{recipient: recipient, category: pushApprovals, body: body, url: base + "/approvals/" + e.ID, actions: actions})521521 }522522 return notices523523 }@@ -10,7 +10,6 @@ import (
1010 "encoding/json"1111 "errors"1212 "fmt"13- "strings"1413 "time"1514 1615 "github.com/FelineStateMachine/tinyrelay/internal/event"@@ -169,7 +168,7 @@ func (t *Tenant) browseApproval(ctx context.Context, actor, id string) (any, err
169168 if !ok {170169 return nil, errors.New("not found: request")171170 }172- item := approvalItemFrom(rows[0], kind, strings.TrimRight(t.publicURL, "/"))171+ item := t.approvalItemFrom(ctx, rows[0], kind)173172 if item.Asker != actor && !containsString(item.Asked, actor) {174173 return nil, errors.New("restricted: this request is not addressed to you")175174 }@@ -138,7 +138,7 @@ func TestBrowseApprovalsStatesAndAccess(t *testing.T) {
138138 if item["state"] != "open" || item["type"] != "approve" || item["subject"] != "Publish release notes 1.4" || item["asker"] != asker || item["expires"] != float64(now+3600) || item["answer"] != nil {139139 t.Fatalf("open item %v", item)140140 }141- if about := item["about"].(map[string]any); about["coordinate"] != "30617:"+owner+":notes" || !strings.HasSuffix(about["url"].(string), "/repo?owner="+owner+"&repo=notes&view=home") {141+ if about := item["about"].(map[string]any); about["coordinate"] != "30617:"+owner+":notes" || !strings.HasSuffix(about["url"].(string), "/repos/"+owner+"/notes") {142142 t.Fatalf("about %v", about)143143 }144144 item = approvalByID(items, decided.ID)@@ -237,7 +237,7 @@ func TestBrowseApprovalsIncludesWikiProposalsAndMerges(t *testing.T) {
237237 t.Fatalf("proposal approval %v", proposalItem)238238 }239239 mergeItem := approvalByID(items, merge.ID)240- if mergeItem["type"] != approvalWikiMerge || mergeItem["state"] != "open" || !strings.Contains(mergeItem["about"].(map[string]any)["url"].(string), "?merge="+merge.ID) {240+ if mergeItem["type"] != approvalWikiMerge || mergeItem["state"] != "open" || !strings.Contains(mergeItem["about"].(map[string]any)["url"].(string), "/proposals/"+merge.ID) {241241 t.Fatalf("merge approval %v", mergeItem)242242 }243243 if got, err := approvalCall(t, tenant, member, "browseapprovals", map[string]any{}); err != nil || len(wikiItems(got, "items")) != 0 {@@ -283,7 +283,7 @@ func TestApprovalRequestsWakeDevicesWithActionsAndCountOnBadge(t *testing.T) {
283283 284284 request := event.Event{Kind: kindComment, PubKey: asker, CreatedAt: now, Tags: [][]string{{"request", "decide"}, {"p", owner}, {"subject", "Publish release notes"}, {"expiration", strconv.FormatInt(now+3600, 10)}}, Content: "Long body"}285285 notices := tenant.pushNotices(ctx, request)286- if len(notices) != 1 || notices[0].category != pushApprovals || notices[0].recipient != owner || notices[0].body != asker[:12]+" asks: Publish release notes" || !strings.HasSuffix(notices[0].url, "/approvals?id="+request.ID) {286+ if len(notices) != 1 || notices[0].category != pushApprovals || notices[0].recipient != owner || notices[0].body != asker[:12]+" asks: Publish release notes" || !strings.HasSuffix(notices[0].url, "/approvals/"+request.ID) {287287 t.Fatalf("decision notices %+v", notices)288288 }289289 if actions := notices[0].actions; len(actions) != 3 || actions[0].Action != "approve" || actions[1].Action != "deny" || actions[2].Action != "reply" || actions[0].Title != "Approve" {@@ -348,7 +348,7 @@ func TestNativeFreeformAllowsBothOptionsAndCustomText(t *testing.T) {
348348 if selection != "text" {349349 e.Tags = append(e.Tags, []string{"option", "c0", "Continue"})350350 }351- item := approvalItemFrom(e, "question", "")351+ item := (&Tenant{}).approvalItemFrom(context.Background(), e, "question")352352 answer := event.Event{Kind: 1111, CreatedAt: 110, Tags: [][]string{{"h", "lab"}, {"e", e.ID}, {"E", e.ID}, {"p", e.PubKey}, {"P", e.PubKey}, {"k", "9"}, {"K", "9"}}}353353 valid := []string{`{"text":"custom"}`, "c0"}354354 invalid := []string{`{"text":"custom","extra":true}`, `{"Text":"custom"}`, `{"text":"one","text":"two"}`, "unknown", `[]`}@@ -405,7 +405,7 @@ func TestNativeFreeformAcceptsChoiceAndExplanationTogether(t *testing.T) {
405405 e := base406406 e.Tags = append([][]string(nil), base.Tags...)407407 e.Tags = append(e.Tags, []string{"selection", test.selection})408- item := approvalItemFrom(e, "question", "")408+ item := (&Tenant{}).approvalItemFrom(context.Background(), e, "question")409409 answer := event.Event{Kind: kindComment, CreatedAt: 110, Content: test.content, Tags: answerTags}410410 if got := nativeAnswerValid(item, answer); got != test.valid {411411 t.Fatalf("nativeAnswerValid = %v, want %v for %s", got, test.valid, test.content)@@ -57,7 +57,7 @@ func (t *Tenant) browseChatActivity(ctx context.Context, actor string, params []
5757 if event.IsJobRequest(row.Kind) && t.Policy().Features.Jobs && (event.Expiration(row) == 0 || event.Expiration(row) > now) {5858 jobs = append(jobs, jobItemFrom(row))5959 } else if kind, ok := approvalRequest(row); ok && (row.PubKey == actor || containsString(approvalAsked(row), actor)) {60- approvals = append(approvals, approvalItemFrom(row, kind, t.publicURL))60+ approvals = append(approvals, t.approvalItemFrom(ctx, row, kind))6161 }6262 }6363 // Keep the inline panel bounded even when a busy room has accumulated a@@ -291,15 +291,15 @@ func (t *Tenant) folderItems(rows []map[string]any, excluded map[string]bool, q
291291 m["name"] = path.Base(p)292292 }293293 m["kind"] = "file"294- m["open"] = "/file?hash=" + url.QueryEscape(m["sha256"].(string))294+ m["open"] = "/file/" + url.PathEscape(m["sha256"].(string))295295 if contextName, ok := m["context"].(string); ok {296- m["context_url"] = "/files?view=" + contextName + "s&path=" + url.QueryEscape(path.Dir(p))296+ m["context_url"] = filesPageURL(contextName+"s", path.Dir(p))297297 }298298 files = append(files, m)299299 }300300 items := []map[string]any{}301301 for n := range folders {302- items = append(items, map[string]any{"kind": "folder", "name": n, "path": strings.Trim(prefix+n, "/"), "open": "/files?view=" + url.QueryEscape(view) + "&path=" + url.QueryEscape(strings.Trim(prefix+n, "/"))})302+ items = append(items, map[string]any{"kind": "folder", "name": n, "path": strings.Trim(prefix+n, "/"), "open": filesPageURL(view, strings.Trim(prefix+n, "/"))})303303 }304304 items = append(items, files...)305305 sort.Slice(items, func(i, j int) bool { return fileItemKey(items[i]) < fileItemKey(items[j]) })@@ -326,7 +326,7 @@ func fileItemKey(item map[string]any) string {
326326 }327327 func breadcrumbs(view, p string) []map[string]string {328328 label := map[string]string{"library": "My files", "sites": "Sites", "rooms": "Rooms", "storage": "Storage"}[view]329- out := []map[string]string{{"name": label, "open": "/files?view=" + view}}329+ out := []map[string]string{{"name": label, "open": filesPageURL(view, "")}}330330 parts := strings.Split(strings.Trim(p, "/"), "/")331331 cur := ""332332 for _, v := range parts {@@ -337,11 +337,24 @@ func breadcrumbs(view, p string) []map[string]string {
337337 cur += "/"338338 }339339 cur += v340- out = append(out, map[string]string{"name": v, "open": "/files?view=" + view + "&path=" + url.QueryEscape(cur)})340+ out = append(out, map[string]string{"name": v, "open": filesPageURL(view, cur)})341341 }342342 return out343343 }344344 345+// filesPageURL is the files page for a view and folder. The library is the346+// bare /files page; the other views are /files/{view}.347+func filesPageURL(view, folder string) string {348+ page := "/files"349+ if view != "library" {350+ page += "/" + url.PathEscape(view)351+ }352+ if folder == "" || folder == "." {353+ return page354+ }355+ return page + "?path=" + url.QueryEscape(folder)356+}357+345358 func (t *Tenant) contextHashes(ctx context.Context, actor string) (map[string][]string, map[string][]string, error) {346359 sitesH := map[string][]string{}347360 roomsH := map[string][]string{}@@ -284,7 +284,7 @@ func (t *Tenant) collaborationProposalNotices(ctx context.Context, e event.Event
284284 case event.KIND_GIT_PATCH:285285 what, subject = "patch", collaborationTitle(e)286286 }287- notice := pushNotice{category: pushApprovals, body: name + " proposes " + what + ": " + excerpt(subject), url: t.collaborationProposalURL(e, r)}287+ notice := pushNotice{category: pushApprovals, body: name + " proposes " + what + ": " + excerpt(subject), url: t.collaborationProposalURL(ctx, e, r)}288288 var notices []pushNotice289289 for _, recipient := range uniqueStrings(t.maintainerKeys(ctx, r)) {290290 if recipient == e.PubKey {@@ -300,23 +300,23 @@ func (t *Tenant) collaborationProposalNotices(ctx context.Context, e event.Event
300300 // collaborationProposalURL is the page a proposal is read on: the issue or301301 // pull request page for a root and for a comment under one, and the event302302 // page otherwise.303-func (t *Tenant) collaborationProposalURL(e event.Event, r gitrelay.Repository) string {303+func (t *Tenant) collaborationProposalURL(ctx context.Context, e event.Event, r gitrelay.Repository) string {304304 base := strings.TrimRight(t.publicURL, "/")305- page := func(view, id string) string {306- return base + "/repo?owner=" + r.Owner + "&repo=" + r.Identifier + "&view=" + view + "&id=" + id305+ page := func(section, id string) string {306+ return t.repoItemURL(ctx, r.Owner, r.Identifier, section, id)307307 }308308 switch e.Kind {309309 case event.KIND_GIT_ISSUE:310- return page("issue", e.ID)310+ return page("issues", e.ID)311311 case event.KIND_GIT_PR:312- return page("pr", e.ID)312+ return page("prs", e.ID)313313 case 1111:314314 if root := event.Tag(e, "E"); len(root) == 64 {315315 switch event.Tag(e, "K") {316316 case "1621":317- return page("issue", root)317+ return page("issues", root)318318 case "1618":319- return page("pr", root)319+ return page("prs", root)320320 }321321 }322322 }@@ -391,7 +391,7 @@ func TestRepositoryProposalWakesOwnerAndMaintainerDevices(t *testing.T) {
391391 if err := json.Unmarshal([]byte(payload), ¬ice); err != nil {392392 t.Fatal(err)393393 }394- if notice.Kind != pushApprovals || notice.Text != "helper proposes issue: Needs review" || notice.URL != "http://relay.test/repo?owner="+k.owner+"&repo="+proposalRepo+"&view=issue&id="+issue.ID {394+ if notice.Kind != pushApprovals || notice.Text != "helper proposes issue: Needs review" || notice.URL != "http://relay.test/repos/"+k.owner+"/"+proposalRepo+"/issues/"+issue.ID {395395 t.Fatalf("notice %+v", notice)396396 }397397 }@@ -404,11 +404,11 @@ func TestRepositoryProposalWakesOwnerAndMaintainerDevices(t *testing.T) {
404404 // are read directly since the approvals category coalesces.405405 comment := proposalComment(t, tenant, k, testAgentSecret, issue, "Also this line looks wrong", now-70)406406 notices, ok := tenant.collaborationProposalNotices(ctx, comment)407- if !ok || len(notices) != 3 || notices[0].recipient != k.owner || notices[0].category != pushApprovals || notices[0].body != "helper proposes comment: Also this line looks wrong" || notices[0].url != "http://relay.test/repo?owner="+k.owner+"&repo="+proposalRepo+"&view=issue&id="+issue.ID {407+ if !ok || len(notices) != 3 || notices[0].recipient != k.owner || notices[0].category != pushApprovals || notices[0].body != "helper proposes comment: Also this line looks wrong" || notices[0].url != "http://relay.test/repos/"+k.owner+"/"+proposalRepo+"/issues/"+issue.ID {408408 t.Fatalf("comment notices %v %+v", ok, notices)409409 }410410 pull := proposalRoot(t, tenant, k, testAgentSecret, event.KIND_GIT_PR, "Add tests", now-60)411- if notices, ok = tenant.collaborationProposalNotices(ctx, pull); !ok || notices[0].body != "helper proposes pull request: Add tests" || notices[0].url != "http://relay.test/repo?owner="+k.owner+"&repo="+proposalRepo+"&view=pr&id="+pull.ID {411+ if notices, ok = tenant.collaborationProposalNotices(ctx, pull); !ok || notices[0].body != "helper proposes pull request: Add tests" || notices[0].url != "http://relay.test/repos/"+k.owner+"/"+proposalRepo+"/prs/"+pull.ID {412412 t.Fatalf("pull notices %v %+v", ok, notices)413413 }414414 if _, ok = tenant.collaborationProposalNotices(ctx, proposalRoot(t, tenant, k, testOwnerSecret, event.KIND_GIT_PR, "Owner pull", now-50)); ok {@@ -151,7 +151,7 @@ func TestGrantRequestNotificationRequiresReview(t *testing.T) {
151151 agent, _ := event.PublicKey(testAgentSecret)152152 request := approvalGrantRequest(t, agentGrantEvent(t, agent, now-30, now+3600, []string{"k", "9"}), now-20)153153 notices := tenant.pushNotices(context.Background(), request)154- if len(notices) != 1 || notices[0].recipient != tenant.Policy().Owner || notices[0].category != pushApprovals || len(notices[0].actions) != 1 || notices[0].actions[0].Action != "review" || !strings.Contains(notices[0].url, "/approvals?id="+request.ID) {154+ if len(notices) != 1 || notices[0].recipient != tenant.Policy().Owner || notices[0].category != pushApprovals || len(notices[0].actions) != 1 || notices[0].actions[0].Action != "review" || !strings.Contains(notices[0].url, "/approvals/"+request.ID) {155155 t.Fatalf("grant request notification: %+v", notices)156156 }157157 }@@ -68,7 +68,7 @@ func TestMemberFileAccessAcrossHTTPAndBrowseSurfaces(t *testing.T) {
6868 }6969 7070 for _, actor := range []string{"bob", "stranger"} {71- request := httptest.NewRequest(http.MethodGet, "http://relay.test/file?hash="+descriptor.Hash, nil)71+ request := httptest.NewRequest(http.MethodGet, "http://relay.test/file/"+descriptor.Hash, nil)7272 if actor != "" {7373 signRequestWithSecret(t, request, "", h.secrets[actor])7474 }@@ -0,0 +1,36 @@
1+package daemon2+3+import (4+ "context"5+ "net/url"6+ "strings"7+)8+9+// personSegment is how a person is written in a page path: the member name10+// when the key has one, else the hex pubkey.11+func (t *Tenant) personSegment(ctx context.Context, pubkey string) string {12+ if t.community == nil {13+ return pubkey14+ }15+ name, err := t.community.MemberName(ctx, pubkey)16+ if err != nil || name == "" {17+ return pubkey18+ }19+ return url.PathEscape(name)20+}21+22+// repoPageURL is the repository home page under the public URL.23+func (t *Tenant) repoPageURL(ctx context.Context, owner, identifier string) string {24+ return strings.TrimRight(t.publicURL, "/") + "/repos/" + t.personSegment(ctx, owner) + "/" + url.PathEscape(identifier)25+}26+27+// repoItemURL is the page of an issue ("issues") or pull request ("prs")28+// in the repository.29+func (t *Tenant) repoItemURL(ctx context.Context, owner, identifier, section, id string) string {30+ return t.repoPageURL(ctx, owner, identifier) + "/" + section + "/" + id31+}32+33+// wikiProposalURL is the merge request compare page of a wiki page.34+func (t *Tenant) wikiProposalURL(d, id string) string {35+ return strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(d) + "/proposals/" + id36+}@@ -0,0 +1,41 @@
1+package daemon2+3+import (4+ "context"5+ "strings"6+ "testing"7+8+ "github.com/FelineStateMachine/tinyrelay/internal/event"9+)10+11+func TestRepoURLsUseMemberNameWhenPresent(t *testing.T) {12+ _, tenant := testTenant(t)13+ ctx := context.Background()14+ owner := tenant.Policy().Owner15+ named := wikiKey(t, testMemberSecret)16+ wikiMember(t, tenant, named)17+ base := strings.TrimRight(tenant.publicURL, "/")18+ // The owner has no member name, so the hex key stays in the path.19+ if got := tenant.repoPageURL(ctx, owner, "notes"); got != base+"/repos/"+owner+"/notes" {20+ t.Fatalf("owner repo url %q", got)21+ }22+ // A named member is written by name, escaped for the path.23+ if got := tenant.repoPageURL(ctx, named, "my notes"); got != base+"/repos/member-"+named[:8]+"/my%20notes" {24+ t.Fatalf("member repo url %q", got)25+ }26+ if got := tenant.repoItemURL(ctx, named, "notes", "issues", strings.Repeat("1", 64)); got != base+"/repos/member-"+named[:8]+"/notes/issues/"+strings.Repeat("1", 64) {27+ t.Fatalf("member issue url %q", got)28+ }29+ // A request about a pull request in the member's repository resolves30+ // the same way.31+ pull := strings.Repeat("2", 64)32+ e := event.Event{Kind: kindComment, Tags: [][]string{{"request", "approve"}, {"a", "30617:" + named + ":notes"}, {"E", pull}, {"K", "1618"}}}33+ if about := tenant.approvalAboutFrom(ctx, e); about == nil || about.URL != base+"/repos/member-"+named[:8]+"/notes/prs/"+pull {34+ t.Fatalf("about %+v", about)35+ }36+ // A stranger with no membership is written as hex too.37+ stranger := strings.Repeat("f", 64)38+ if got := tenant.repoPageURL(ctx, stranger, "notes"); got != base+"/repos/"+stranger+"/notes" {39+ t.Fatalf("stranger repo url %q", got)40+ }41+}@@ -144,7 +144,7 @@ func TestDeviceNotificationsFollowCategoriesAndBadge(t *testing.T) {
144144 t.Fatalf("message notices %+v", notices)145145 }146146 issue := event.Event{Kind: 1621, PubKey: other, CreatedAt: 1, Tags: [][]string{{"a", "30617:" + owner + ":notes"}, {"subject", "Broken build"}}}147- if notices := tenant.pushNotices(ctx, issue); len(notices) != 1 || notices[0].category != pushReplies || notices[0].body != "New issue: Broken build" || !strings.Contains(notices[0].url, "view=issue") {147+ if notices := tenant.pushNotices(ctx, issue); len(notices) != 1 || notices[0].category != pushReplies || notices[0].body != "New issue: Broken build" || !strings.Contains(notices[0].url, "/repos/"+owner+"/notes/issues/") {148148 t.Fatalf("issue notices %+v", notices)149149 }150150 @@ -116,15 +116,15 @@ func (t *Tenant) pushNotices(ctx context.Context, e event.Event) []pushNotice {
116116 return nil117117 }118118 label := "New issue"119- view := "issue"119+ section := "issues"120120 if e.Kind == event.KIND_GIT_PR {121- label, view = "New pull request", "pr"121+ label, section = "New pull request", "prs"122122 }123123 body := label124124 if subject := strings.TrimSpace(event.Tag(e, "subject")); subject != "" {125125 body += ": " + excerpt(subject)126126 }127- notices = append(notices, pushNotice{recipient: owner, category: pushReplies, body: body, url: base + "/repo?owner=" + owner + "&repo=" + identifier + "&view=" + view + "&id=" + e.ID})127+ notices = append(notices, pushNotice{recipient: owner, category: pushReplies, body: body, url: t.repoItemURL(ctx, owner, identifier, section, e.ID)})128128 case 1, 1111, 30023:129129 authored := t.referencedAuthors(ctx, e)130130 for _, recipient := range pushRecipients(e) {@@ -8,7 +8,6 @@ package daemon
88 99 import (1010 "context"11- "net/url"1211 "sort"1312 "strconv"1413 "strings"@@ -384,7 +383,7 @@ func (t *Tenant) notifyWikiMerge(ctx context.Context, e event.Event) {
384383 if err == nil && len(rows.Events) == 1 {385384 title = wikiVersionFrom(rows.Events[0], false).Title386385 }387- notice := pushNotice{recipient: m.Destination, category: pushReplies, body: "Merge request for " + excerpt(title), url: strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(m.TargetD) + "?merge=" + e.ID}386+ notice := pushNotice{recipient: m.Destination, category: pushReplies, body: "Merge request for " + excerpt(title), url: t.wikiProposalURL(m.TargetD, e.ID)}388387 if err := t.enqueuePushNotice(ctx, notice); err != nil {389388 t.app.telemetry.Logger().Debug("wiki merge notification not queued", "error", err)390389 }@@ -53,7 +53,7 @@ func (t *Tenant) wikiProposalApprovalItems(ctx context.Context, actor string, de
5353 continue5454 }5555 version := wikiVersionFrom(row, false)56- item := approvalItemFrom(row, approvalWikiProposal, strings.TrimRight(t.publicURL, "/"))56+ item := t.approvalItemFrom(ctx, row, approvalWikiProposal)5757 item.Asked = asked5858 item.Subject = version.Title5959 item.About = &approvalAbout{Coordinate: version.Coordinate, Event: row.ID, Kind: "30818", URL: strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(version.D) + "?version=" + url.QueryEscape(row.ID)}@@ -91,10 +91,10 @@ func (t *Tenant) wikiMergeApprovalItems(ctx context.Context, actor string, befor
9191 if merge.Destination != actor {9292 continue9393 }94- item := approvalItemFrom(row, approvalWikiMerge, strings.TrimRight(t.publicURL, "/"))94+ item := t.approvalItemFrom(ctx, row, approvalWikiMerge)9595 item.Asked = []string{actor}9696 item.Subject = "Wiki merge request"97- item.About = &approvalAbout{Coordinate: merge.Target, Event: row.ID, Kind: "818", URL: strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(merge.TargetD) + "?merge=" + row.ID}97+ item.About = &approvalAbout{Coordinate: merge.Target, Event: row.ID, Kind: "818", URL: t.wikiProposalURL(merge.TargetD, row.ID)}9898 items = append(items, item)9999 }100100 answers, err := t.approvalAnswers(ctx, items, nowUnix())@@ -183,7 +183,7 @@ func (t *Tenant) wikiApprovalByID(ctx context.Context, actor, id string) (approv
183183 return approvalItem{}, false, nil184184 }185185 version := wikiVersionFrom(row, false)186- item := approvalItemFrom(row, approvalWikiProposal, strings.TrimRight(t.publicURL, "/"))186+ item := t.approvalItemFrom(ctx, row, approvalWikiProposal)187187 item.Asked = t.wikiApprovalAudience(ctx)188188 item.Subject = version.Title189189 item.About = &approvalAbout{Coordinate: version.Coordinate, Event: row.ID, Kind: "30818", URL: strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(version.D) + "?version=" + url.QueryEscape(row.ID)}@@ -193,10 +193,10 @@ func (t *Tenant) wikiApprovalByID(ctx context.Context, actor, id string) (approv
193193 if merge.Destination == "" || merge.Destination != actor {194194 return approvalItem{}, false, nil195195 }196- item := approvalItemFrom(row, approvalWikiMerge, strings.TrimRight(t.publicURL, "/"))196+ item := t.approvalItemFrom(ctx, row, approvalWikiMerge)197197 item.Asked = []string{actor}198198 item.Subject = "Wiki merge request"199- item.About = &approvalAbout{Coordinate: merge.Target, Event: row.ID, Kind: "818", URL: strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(merge.TargetD) + "?merge=" + url.QueryEscape(row.ID)}199+ item.About = &approvalAbout{Coordinate: merge.Target, Event: row.ID, Kind: "818", URL: t.wikiProposalURL(merge.TargetD, row.ID)}200200 return item, true, nil201201 }202202 @@ -338,7 +338,7 @@ func TestWikiMergeRequestWakesDestinationDevices(t *testing.T) {
338338 if err := json.Unmarshal([]byte(payload), ¬ice); err != nil {339339 t.Fatal(err)340340 }341- if notice.Kind != pushReplies || notice.Text != "Merge request for Bitcoin Basics" || notice.URL != "http://relay.test/wiki/bitcoin-basics?merge="+merge.ID {341+ if notice.Kind != pushReplies || notice.Text != "Merge request for Bitcoin Basics" || notice.URL != "http://relay.test/wiki/bitcoin-basics/proposals/"+merge.ID {342342 t.Fatalf("notice %+v", notice)343343 }344344 }