clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

show wiki revision numbers, the history of revisions and the approved fallback in the web ui

Dami committed d985fba
commit
d985fba24fa50271a3e7ca42149298887194b397
parent
602d2af64f0d23cd7a22532bc624a129486a5ad3
author
Dami <dami.etoile@gmail.com>
4 files changed, +122 -33
Minternal/webui/style.css2+-
Minternal/webui/wiki.go49++++++++++++----
Minternal/webui/wiki.html15+++--
Minternal/webui/wiki_test.go89++++++++++++++++++++++++++----
M internal/webui/style.css +1 -1
@@ -230,7 +230,7 @@ merge-request,wiki-proposal,collaboration-proposal{display:block;border:1px soli
230230 merge-request nostr-react,merge-request nostr-react form,wiki-proposal nostr-react,wiki-proposal nostr-react form,collaboration-proposal nostr-react,collaboration-proposal nostr-react form{display:inline}merge-request nostr-react output,wiki-proposal nostr-react output,collaboration-proposal nostr-react output{display:inline-block;margin-left:6px;font:12px var(--mono);color:var(--muted)}merge-request nostr-react output[data-error],wiki-proposal nostr-react output[data-error],collaboration-proposal nostr-react output[data-error]{color:var(--alert)}231231 wiki-proposal[data-state=pending],collaboration-proposal[data-state=pending]{border-color:var(--bark)}wiki-proposal[data-state=rejected],collaboration-proposal[data-state=rejected]{border-color:var(--alert)}#wiki-list #rows td:first-child small,#panel li[data-state]{font:12px var(--mono)}#wiki-list #rows td:first-child small{margin-left:6px}#wiki-list #rows tr[data-state=pending] small,#panel li[data-state=pending]{color:var(--bark)}#wiki-list #rows tr[data-state=approved] small,#panel li[data-state=approved]{color:var(--accent)}#wiki-list #rows tr[data-state=rejected] small,#panel li[data-state=rejected]{color:var(--alert)}232232 #events li>collaboration-proposal,diff-view td>collaboration-proposal{margin:6px 0 8px;max-width:none}#collaboration-list #rows tr[data-state] td:nth-child(2)>small:first-of-type{font:12px var(--mono)}#collaboration-list #rows tr[data-state=pending] td:nth-child(2)>small:first-of-type{color:var(--bark)}#collaboration-list #rows tr[data-state=approved] td:nth-child(2)>small:first-of-type{color:var(--accent)}#collaboration-list #rows tr[data-state=rejected] td:nth-child(2)>small:first-of-type{color:var(--alert)}233-#merge-status{font:12px var(--mono);color:var(--muted)}#wiki-compare h2{margin-top:18px}#wiki-redirects{font:12px var(--mono);color:var(--muted)}233+#merge-status{font:12px var(--mono);color:var(--muted)}#wiki-compare h2{margin-top:18px}#wiki-redirects,#wiki-revision-note{font:12px var(--mono);color:var(--muted)}#wiki-revision-note{margin:0 0 10px}wiki-proposal>small{display:block;margin-top:4px;font:12px var(--mono);color:var(--muted)}#wiki-page #rows td small{font:12px var(--mono);color:var(--muted)}234234 wiki-compose{display:block;max-width:720px;border-bottom:1px solid var(--line);padding:10px 0}235235 wiki-compose label{display:inline-block;margin:0 10px 6px 0;vertical-align:top;font:13px var(--sans);color:var(--muted)}wiki-compose label input{display:block;margin-top:2px;font:13px var(--mono);min-width:200px}236236 wiki-compose label:has(textarea){display:block}wiki-compose textarea{min-height:14em}wiki-compose button{margin-right:6px}
M internal/webui/wiki.go +37 -12
@@ -13,23 +13,37 @@ import (
1313 1414 // wikiView is one page as the templates see it. Version is the version1515 // being shown, Number its place counted from the oldest, Others the rest.16+// Revision and Revisions number the shown version among its author's17+// revisions; History lists every revision the viewer may see, newest18+// first. Archived is set when an older revision was opened by id, and19+// ReadersRevision names the approved revision readers see while the20+// shown one is a proposal that is not approved.1621 type wikiView struct {17- D, Title string18- Version map[string]any19- Author string20- Number, Count int21- Forks, Links int22- Versions []map[string]any23- Others []map[string]any24- Merges []map[string]any25- OpenMerges []map[string]any26- OpenCount int27- RedirectsTo []map[string]any28- RedirectsFrom []map[string]any22+ D, Title string23+ Version map[string]any24+ Author string25+ Number, Count int26+ Revision, Revisions int27+ Forks, Links int28+ Versions []map[string]any29+ Others []map[string]any30+ History []map[string]any31+ Archived bool32+ ReadersRevision int33+ Merges []map[string]any34+ OpenMerges []map[string]any35+ OpenCount int36+ RedirectsTo []map[string]any37+ RedirectsFrom []map[string]any2938 // CanApprove is set when the viewer may accept or reject proposals.3039 CanApprove bool3140 }3241 42+func wikiInt(value any) int {43+ number, _ := value.(float64)44+ return int(number)45+}46+3347 // wikiProposalView is how a version's proposal state reads for one viewer.3448 // Shown is set for the people who may see the state: the owner and3549 // moderators, and the version's author. Decide is set when the viewer may
@@ -77,14 +91,25 @@ func wikiPageView(result any) wikiView {
7791 view.Merges = wikiMaps(page["merges"])7892 view.RedirectsTo = wikiMaps(page["redirects_to"])7993 view.RedirectsFrom = wikiMaps(page["redirects_from"])94+ view.History = wikiMaps(page["history"])8095 view.CanApprove, _ = page["can_approve"].(bool)8196 view.Count = len(view.Versions)8297 if current := valueMap(page["version"]); current != nil {8398 view.Version = current8499 view.Author = plainString(current["author"])100+ view.Revision, view.Revisions = wikiInt(current["revision"]), wikiInt(current["revisions"])101+ view.Archived = plainString(current["superseded_by"]) != "" && plainString(page["preferred_by"]) == "version"85102 if links, ok := current["links"].([]any); ok {86103 view.Links = len(links)87104 }105+ if current["proposal"] == true && plainString(current["approval"]) != "approved" {106+ for _, h := range view.History {107+ if plainString(h["author"]) == view.Author && plainString(h["approval"]) == "approved" {108+ view.ReadersRevision = wikiInt(h["revision"])109+ break110+ }111+ }112+ }88113 }89114 if view.Title == "" {90115 view.Title = view.D
M internal/webui/wiki.html +8 -7
@@ -23,13 +23,14 @@
2323 <h1>{{$w.Title}}</h1>2424 {{template "alert" .Notice}}{{if not $w.Version}}<p>This name has no article of its own.</p>2525 {{template "wikiRedirects" $w}}{{if .Actor}}{{template "wikiEditor" .}}{{end}}26-{{else}}{{$s := wikiProposal $w.Version $.Actor $w.CanApprove}}{{if $s.Shown}}<wiki-proposal data-state="{{$s.State}}">Proposed by {{template "nostrName" $w.Author}} | {{$s.State}}{{if $s.By}} {{when $s.At}} by {{template "nostrName" $s.By}}{{end}}{{if $s.Decide}}<nostr-react event="{{str (index $w.Version "id")}}" pubkey="{{$w.Author}}" kind="30818"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react>{{end}}</wiki-proposal>26+{{else}}{{$s := wikiProposal $w.Version $.Actor $w.CanApprove}}{{if $s.Shown}}<wiki-proposal data-state="{{$s.State}}">Proposed by {{template "nostrName" $w.Author}} | {{$s.State}}{{if $s.By}} {{when $s.At}} by {{template "nostrName" $s.By}}{{end}}{{if $s.Decide}}<nostr-react event="{{str (index $w.Version "id")}}" pubkey="{{$w.Author}}" kind="30818"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react>{{end}}{{if $w.ReadersRevision}}<small>showing {{$s.State}} revision {{$w.Revision}}; readers see approved revision {{$w.ReadersRevision}}</small>{{end}}</wiki-proposal>2727 {{end}}{{range $m := $w.OpenMerges}}<merge-request>{{template "nostrName" (str (index $m "author"))}} proposes a new version of this page | <a href="{{wikiURL $w.D "merge" (str (index $m "id"))}}">compare</a>{{if eq $.Actor (str (index $m "destination"))}}<nostr-react event="{{str (index $m "id")}}" pubkey="{{str (index $m "author")}}" kind="818"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react>{{end}}<a href="/e/{{str (index $m "id")}}">Reply</a></merge-request>2828 {{end}}{{if .Query.Get "merge"}}{{template "wikiCompare" .}}2929 {{else if .Query.Get "edit"}}{{template "wikiEditor" .}}3030 {{else if .Query.Get "history"}}{{template "wikiHistory" .}}31-{{else}}<wiki-article><header>wiki | {{$w.D}} | by {{template "nostrName" $w.Author}} | version {{$w.Number}} of {{$w.Count}} | {{$w.Forks}} fork{{if ne $w.Forks 1}}s{{end}}</header>32-{{with asMap (index $w.Version "fork")}}<p><small>forked from <a href="{{wikiURL $w.D "version" (str (index . "e"))}}">{{shortID (index . "e")}}</a></small></p>31+{{else}}<wiki-article><header>wiki | {{$w.D}} | revision {{$w.Revision}} by {{template "nostrName" $w.Author}} | version {{$w.Number}} of {{$w.Count}} | {{$w.Forks}} fork{{if ne $w.Forks 1}}s{{end}}</header>32+{{if $w.Archived}}<p id="wiki-revision-note">revision {{$w.Revision}} of {{$w.Revisions}} by this author; a newer revision exists | <a href="{{wikiURL $w.D "author" $w.Author}}">show the newest</a></p>33+{{end}}{{with asMap (index $w.Version "fork")}}<p><small>forked from <a href="{{wikiURL $w.D "version" (str (index . "e"))}}">{{shortID (index . "e")}}</a></small></p>3334 {{end}}{{with asMap (index $w.Version "defer")}}<p><small>the author defers to <a href="{{wikiURL $w.D "version" (str (index . "e"))}}">{{shortID (index . "e")}}</a></small></p>3435 {{end}}{{wikiHTML (index $w.Version "content")}}</wiki-article>3536 {{template "wikiRedirects" $w}}{{end}}{{end}}</section>
@@ -62,18 +63,18 @@
6263 {{else}}<p>The destination author has no version of this page.</p>6364 {{end}}<p><a href="{{wikiURL $w.D}}">back to the page</a></p></div>{{end}}{{end}}6465 65-{{/* wikiHistory lists every version of a page, newest first. */}}66+{{/* wikiHistory lists every revision of a page across authors, newest first. */}}6667 {{define "wikiHistory"}}{{$w := wikiView .Event}}<table id="rows">67-<thead><tr><th>author</th><th>version</th><th>notes</th><th>published</th></tr></thead>68+<thead><tr><th>author</th><th>revision</th><th>version</th><th>notes</th><th>published</th></tr></thead>6869 <tbody>69-{{range $w.Versions}}{{$s := wikiProposal . $.Actor $w.CanApprove}}<tr{{if $s.Shown}} data-state="{{$s.State}}"{{end}}><td>{{template "nostrName" (str (index . "author"))}}</td><td><a href="{{wikiURL $w.D "version" (str (index . "id"))}}">{{shortID (index . "id")}}</a></td><td>{{with asMap (index . "fork")}}fork of {{shortID (index . "e")}}{{end}}{{with asMap (index . "defer")}} defers to {{shortID (index . "e")}}{{end}}{{with index . "likes"}} | {{str .}} likes{{end}}{{if $s.Shown}} | proposed {{$s.State}}{{end}}</td><td>{{when (index . "created_at")}}</td></tr>70+{{range $w.History}}{{$s := wikiProposal . $.Actor $w.CanApprove}}<tr{{if $s.Shown}} data-state="{{$s.State}}"{{end}}><td>{{template "nostrName" (str (index . "author"))}}</td><td>{{str (index . "revision")}} <small>{{if index . "superseded_by"}}replaced{{else}}current{{end}}</small></td><td><a href="{{wikiURL $w.D "version" (str (index . "id"))}}">{{shortID (index . "id")}}</a></td><td>{{with asMap (index . "fork")}}fork of {{shortID (index . "e")}}{{end}}{{with asMap (index . "defer")}} defers to {{shortID (index . "e")}}{{end}}{{with index . "likes"}} | {{str .}} likes{{end}}{{if $s.Shown}} | proposed {{$s.State}}{{end}}</td><td>{{when (index . "created_at")}}</td></tr>7071 {{end}}</tbody>7172 </table>7273 <p><a href="{{wikiURL $w.D}}">back to the page</a></p>{{end}}7374 7475 {{define "panel-wiki"}}{{if eq .Tab "wiki"}}<section><h4>Wiki</h4><table><tr><th>protocol</th><td>NIP-54</td></tr><tr><th>markup</th><td>Djot</td></tr><tr><th>reads</th><td>{{.Policy.Reads}}</td></tr></table></section>7576 <section><h4>Actions</h4><ul>{{if .Actor}}<li><a href="/wiki?new=1">new page</a></li>{{else}}<li><a href="/signin">sign in to write</a></li>{{end}}</ul></section>76-{{else}}{{$w := wikiView .Event}}<section><h4>Page</h4><table><tr><th>name</th><td><code>{{or $w.D (.Query.Get "d")}}</code></td></tr><tr><th>versions</th><td>{{$w.Count}}</td></tr><tr><th>merge requests</th><td>{{$w.OpenCount}} open</td></tr><tr><th>links</th><td>{{$w.Links}}</td></tr></table></section>77+{{else}}{{$w := wikiView .Event}}<section><h4>Page</h4><table><tr><th>name</th><td><code>{{or $w.D (.Query.Get "d")}}</code></td></tr><tr><th>versions</th><td>{{$w.Count}}</td></tr>{{if $w.History}}<tr><th>revisions</th><td>{{len $w.History}}</td></tr>{{end}}<tr><th>merge requests</th><td>{{$w.OpenCount}} open</td></tr><tr><th>links</th><td>{{$w.Links}}</td></tr></table></section>7778 {{if $w.Others}}<section><h4>Other versions</h4><ul>{{range $w.Others}}{{$s := wikiProposal . $.Actor $w.CanApprove}}<li{{if $s.Shown}} data-state="{{$s.State}}"{{end}}><a href="{{wikiURL $w.D "version" (str (index . "id"))}}">{{shortID (index . "author")}}</a>{{with asMap (index . "fork")}} | fork{{end}}{{if $s.Shown}} | proposed {{$s.State}}{{end}} <small>{{short (index . "created_at")}}</small></li>{{end}}</ul></section>7879 {{end}}{{if $w.RedirectsTo}}<section><h4>Also known as</h4><ul>{{range $w.RedirectsTo}}<li><code>{{str (index . "d")}}</code></li>{{end}}</ul></section>7980 {{end}}<section><h4>Actions</h4><ul>{{if $w.Version}}{{if eq .Actor $w.Author}}<li><a href="{{wikiURL $w.D "edit" "1"}}">edit</a></li>{{else if .Actor}}<li><a href="{{wikiURL $w.D "edit" "1"}}">fork</a></li>{{end}}<li><a href="{{wikiURL $w.D "history" "1"}}">history</a></li>{{end}}{{if .Actor}}<li><a href="/wiki?new=1">new page</a></li>{{else}}<li><a href="/signin">sign in to write</a></li>{{end}}</ul></section>{{end}}{{end}}
M internal/webui/wiki_test.go +76 -13
@@ -18,6 +18,10 @@ const (
1818 wikiMerge = "3333333333333333333333333333333333333333333333333333333333333333"1919 wikiOwnerV = "4444444444444444444444444444444444444444444444444444444444444444"2020 wikiOtherV = "5555555555555555555555555555555555555555555555555555555555555555"21+ // wikiOwnerOld and wikiOtherOld are archived revisions that the22+ // current versions replaced.23+ wikiOwnerOld = "7777777777777777777777777777777777777777777777777777777777777777"24+ wikiOtherOld = "8888888888888888888888888888888888888888888888888888888888888888"2125 )2226 2327 // wikiBackend answers the three wiki queries with one page that has two
@@ -29,11 +33,15 @@ type wikiBackend struct {
2933 private bool3034 // proposal makes the second author's version a proposal in this state3135 // (pending, approved or rejected), decided by the owner when it is not32- // pending. approver marks the caller as allowed to decide, and shown33- // makes the proposal the page's shown version.36+ // pending, with an approved earlier revision behind it in the history.37+ // approver marks the caller as allowed to decide, and shown makes the38+ // proposal the page's shown version.3439 proposal string3540 approver bool3641 shown bool42+ // archived gives the owner's version an earlier revision, which a43+ // request naming it by version opens.44+ archived bool3745 }3846 3947 func (b *wikiBackend) Query(_ context.Context, method string, params []json.RawMessage, actor string) (any, error) {
@@ -49,20 +57,31 @@ func (b *wikiBackend) Query(_ context.Context, method string, params []json.RawM
4957 if b.private && actor == "" {5058 return nil, errors.New("auth-required: sign in to read this relay")5159 }52- owner := map[string]any{"id": wikiOwnerV, "author": wikiOwner, "created_at": float64(1788876000), "d": "release-notes-1-4", "title": "Release notes 1.4 <b>", "summary": "What changed", "coordinate": "30818:" + wikiOwner + ":release-notes-1-4", "likes": float64(0),60+ owner := map[string]any{"id": wikiOwnerV, "author": wikiOwner, "created_at": float64(1788876000), "d": "release-notes-1-4", "title": "Release notes 1.4 <b>", "summary": "What changed", "coordinate": "30818:" + wikiOwner + ":release-notes-1-4", "likes": float64(0), "revision": float64(1), "revisions": float64(1),5361 "content": "# Highlights\n\nPrivate tenants and _encrypted_ uploads. See [[Files and Private Repositories]].\n\n<script>alert(1)</script>", "links": []any{"files-and-private-repositories"}}54- other := map[string]any{"id": wikiOtherV, "author": wikiOther, "created_at": float64(1788880000), "d": "release-notes-1-4", "title": "Release notes 1.4", "coordinate": "30818:" + wikiOther + ":release-notes-1-4", "likes": float64(0), "fork": map[string]any{"a": owner["coordinate"], "e": wikiOwnerV}, "content": "# Highlights\n\nA proposed rewrite."}62+ other := map[string]any{"id": wikiOtherV, "author": wikiOther, "created_at": float64(1788880000), "d": "release-notes-1-4", "title": "Release notes 1.4", "coordinate": "30818:" + wikiOther + ":release-notes-1-4", "likes": float64(0), "revision": float64(1), "revisions": float64(1), "fork": map[string]any{"a": owner["coordinate"], "e": wikiOwnerV}, "content": "# Highlights\n\nA proposed rewrite."}5563 merge := map[string]any{"id": wikiMerge, "author": wikiOther, "created_at": float64(1788881000), "content": "Please take my rewrite", "target": owner["coordinate"], "target_d": "release-notes-1-4", "destination": wikiOwner, "source": wikiOtherV, "status": "open"}56- shown := owner64+ shown, preferredBy := owner, "owner"65+ history := []any{other, owner}5766 if b.proposal != "" {58- other["proposal"], other["approval"] = true, b.proposal67+ other["proposal"], other["approval"], other["revision"], other["revisions"] = true, b.proposal, float64(2), float64(2)5968 if b.proposal != "pending" {6069 other["approval_event"], other["approval_at"], other["approval_by"] = wikiMerge, float64(1788882000), wikiOwner6170 }71+ old := map[string]any{"id": wikiOtherOld, "author": wikiOther, "created_at": float64(1788878000), "d": "release-notes-1-4", "title": "Release notes 1.4", "coordinate": other["coordinate"], "likes": float64(0), "revision": float64(1), "revisions": float64(2), "superseded_by": wikiOtherV, "proposal": true, "approval": "approved", "approval_event": wikiMerge, "approval_at": float64(1788879000), "approval_by": wikiOwner}72+ history = []any{other, old, owner}6273 if b.shown {6374 shown = other6475 }6576 }77+ if b.archived {78+ owner["revision"], owner["revisions"] = float64(2), float64(2)79+ old := map[string]any{"id": wikiOwnerOld, "author": wikiOwner, "created_at": float64(1788870000), "d": "release-notes-1-4", "title": "Release notes 1.3", "coordinate": owner["coordinate"], "likes": float64(1), "revision": float64(1), "revisions": float64(2), "superseded_by": wikiOwnerV, "content": "# Highlights\n\nThe first draft."}80+ history = append(history, old)81+ if object["version"] == wikiOwnerOld {82+ shown, preferredBy = old, "version"83+ }84+ }6685 switch method {6786 case "browsewiki":6887 return map[string]any{"items": []any{map[string]any{"d": "release-notes-1-4", "title": "Release notes 1.4 <b>", "version": shown, "versions": float64(2), "open_merges": float64(1)}}, "next_cursor": "release-notes-1-4", "can_approve": b.approver}, nil
@@ -70,7 +89,7 @@ func (b *wikiBackend) Query(_ context.Context, method string, params []json.RawM
7089 if object["d"] != "release-notes-1-4" {7190 return nil, errors.New("not found: wiki page")7291 }73- return map[string]any{"d": "release-notes-1-4", "title": "Release notes 1.4 <b>", "version": shown, "preferred_by": "owner", "versions": []any{other, owner}, "merges": []any{merge}, "redirects_to": []any{map[string]any{"d": "changelog-1-4", "target_d": "release-notes-1-4"}}, "redirects_from": []any{}, "can_approve": b.approver}, nil92+ return map[string]any{"d": "release-notes-1-4", "title": "Release notes 1.4 <b>", "version": shown, "preferred_by": preferredBy, "versions": []any{other, owner}, "history": history, "merges": []any{merge}, "redirects_to": []any{map[string]any{"d": "changelog-1-4", "target_d": "release-notes-1-4"}}, "redirects_from": []any{}, "can_approve": b.approver}, nil7493 case "browsewikimerge":7594 return map[string]any{"merge": merge, "proposed": other, "target": owner}, nil7695 }
@@ -151,7 +170,7 @@ func TestWikiPageRendersArticleMergeBarAndPanel(t *testing.T) {
151170 `<merge-request><nostr-name pubkey="`+wikiOther+`"`, `proposes a new version of this page`, `href="/wiki/release-notes-1-4?merge=`+wikiMerge+`">compare</a>`,152171 `<nostr-react event="`+wikiMerge+`" pubkey="`+wikiOther+`" kind="818"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react>`,153172 `<a href="/e/`+wikiMerge+`">Reply</a>`,154- `<wiki-article><header>wiki | release-notes-1-4 | by <nostr-name pubkey="`+wikiOwner+`"`, `version 1 of 2 | 1 fork</header>`,173+ `<wiki-article><header>wiki | release-notes-1-4 | revision 1 by <nostr-name pubkey="`+wikiOwner+`"`, `version 1 of 2 | 1 fork</header>`,155174 `<h1>Highlights</h1>`, `<em>encrypted</em>`, `<a href="/wiki/files-and-private-repositories">Files and Private Repositories</a>`, `&lt;script&gt;alert(1)&lt;/script&gt;`,156175 `<h4>Page</h4>`, `<th>versions</th><td>2</td>`, `<th>merge requests</th><td>1 open</td>`, `<th>links</th><td>1</td>`,157176 `<h4>Other versions</h4>`, `href="/wiki/release-notes-1-4?version=`+wikiOtherV+`">222222222222</a> | fork`,
@@ -186,7 +205,44 @@ func TestWikiPageCompareEditorAndHistory(t *testing.T) {
186205 187206 path = "/wiki/release-notes-1-4?history=1"188207 body = wikiGet(t, wikiApp(t, b, wikiOwner), path)189- wantAll(t, path, body, `<th>author</th><th>version</th>`, `fork of 444444444444`, `href="/wiki/release-notes-1-4?version=`+wikiOwnerV+`">444444444444</a>`)208+ wantAll(t, path, body, `<th>author</th><th>revision</th><th>version</th>`, `<td>1 <small>current</small></td>`, `fork of 444444444444`, `href="/wiki/release-notes-1-4?version=`+wikiOwnerV+`">444444444444</a>`)209+ wantAll(t, path, body, `<th>revisions</th><td>2</td>`)210+ wantNone(t, path, body, "<small>replaced</small>")211+}212+213+func TestWikiHistoryListsRevisionsWithStates(t *testing.T) {214+ b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, proposal: "pending", approver: true, shown: true, archived: true}215+ path := "/wiki/release-notes-1-4?history=1"216+ body := wikiGet(t, wikiApp(t, b, wikiOwner), path)217+ wantAll(t, path, body,218+ `<tr data-state="pending"><td><nostr-name pubkey="`+wikiOther+`"`, `<td>2 <small>current</small></td><td><a href="/wiki/release-notes-1-4?version=`+wikiOtherV+`">555555555555</a></td><td>fork of 444444444444 | proposed pending</td>`,219+ `<tr data-state="approved"><td><nostr-name pubkey="`+wikiOther+`"`, `<td>1 <small>replaced</small></td><td><a href="/wiki/release-notes-1-4?version=`+wikiOtherOld+`">888888888888</a></td><td> | proposed approved</td>`,220+ `<td>2 <small>current</small></td><td><a href="/wiki/release-notes-1-4?version=`+wikiOwnerV+`">444444444444</a></td>`,221+ `<td>1 <small>replaced</small></td><td><a href="/wiki/release-notes-1-4?version=`+wikiOwnerOld+`">777777777777</a></td><td> | 1 likes</td>`,222+ `<th>revisions</th><td>4</td>`)223+ // A member sees the rows without their proposal states.224+ b.approver = false225+ body = wikiGet(t, wikiApp(t, b, wikiMember), path)226+ wantAll(t, path, body, `<td>1 <small>replaced</small></td><td><a href="/wiki/release-notes-1-4?version=`+wikiOtherOld+`">888888888888</a></td><td></td>`)227+ wantNone(t, path, body, "<tr data-state=", "proposed ")228+}229+230+func TestWikiArchivedRevisionShowsANote(t *testing.T) {231+ b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, archived: true}232+ path := "/wiki/release-notes-1-4?version=" + wikiOwnerOld233+ body := wikiGet(t, wikiApp(t, b, wikiOther), path)234+ if b.params["browsewikipage"]["version"] != wikiOwnerOld {235+ t.Fatalf("version lookup params=%v", b.params["browsewikipage"])236+ }237+ wantAll(t, path, body,238+ `<wiki-article><header>wiki | release-notes-1-4 | revision 1 by <nostr-name pubkey="`+wikiOwner+`"`,239+ `<p id="wiki-revision-note">revision 1 of 2 by this author; a newer revision exists | <a href="/wiki/release-notes-1-4?author=`+wikiOwner+`">show the newest</a></p>`,240+ `The first draft.`)241+ // The author's current revision carries no note.242+ path = "/wiki/release-notes-1-4"243+ body = wikiGet(t, wikiApp(t, b, wikiOther), path)244+ wantAll(t, path, body, `revision 2 by <nostr-name pubkey="`+wikiOwner+`"`, `<em>encrypted</em>`)245+ wantNone(t, path, body, `id="wiki-revision-note"`, "The first draft.")190246 }191247 192248 func TestWikiMissingPageOffersTheEditorToMembers(t *testing.T) {
@@ -221,15 +277,22 @@ func TestWikiProposalBarOffersTheDecisionToApprovers(t *testing.T) {
221277 path := "/wiki/release-notes-1-4"222278 body := wikiGet(t, wikiApp(t, b, wikiOwner), path)223279 wantAll(t, path, body,224- `<wiki-proposal data-state="pending">Proposed by <nostr-name pubkey="`+wikiOther+`"`, `| pending<nostr-react event="`+wikiOtherV+`" pubkey="`+wikiOther+`" kind="30818"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react></wiki-proposal>`,225- `<wiki-article><header>wiki | release-notes-1-4 | by <nostr-name pubkey="`+wikiOther+`"`, `A proposed rewrite.`)280+ `<wiki-proposal data-state="pending">Proposed by <nostr-name pubkey="`+wikiOther+`"`, `| pending<nostr-react event="`+wikiOtherV+`" pubkey="`+wikiOther+`" kind="30818"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react><small>showing pending revision 2; readers see approved revision 1</small></wiki-proposal>`,281+ `<wiki-article><header>wiki | release-notes-1-4 | revision 2 by <nostr-name pubkey="`+wikiOther+`"`, `A proposed rewrite.`)226282 // A decided proposal names the decision, its time and the deciding key,227- // and offers no buttons.283+ // and offers no buttons. Readers still see the earlier approved284+ // revision while the edit is rejected, and the edit itself once it is285+ // approved.228286 for _, state := range []string{"approved", "rejected"} {229287 b.proposal = state230288 body = wikiGet(t, wikiApp(t, b, wikiOwner), path)231289 wantAll(t, path, body, `<wiki-proposal data-state="`+state+`">Proposed by <nostr-name pubkey="`+wikiOther+`"`, `| `+state+` 2026-09-08 15:40 UTC by <nostr-name pubkey="`+wikiOwner+`"`)232290 wantNone(t, path, body, "<nostr-react", ">Accept<")291+ if state == "rejected" {292+ wantAll(t, path, body, `<small>showing rejected revision 2; readers see approved revision 1</small></wiki-proposal>`)293+ } else {294+ wantNone(t, path, body, "readers see approved revision")295+ }233296 }234297 // A pending proposal that is not the shown version is marked in the235298 // panel and the history, with no bar above the article.
@@ -245,7 +308,7 @@ func TestWikiProposalAuthorSeesTheStateWithoutButtons(t *testing.T) {
245308 b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, proposal: "pending", shown: true}246309 path := "/wiki/release-notes-1-4"247310 body := wikiGet(t, wikiApp(t, b, wikiOther), path)248- wantAll(t, path, body, `<wiki-proposal data-state="pending">Proposed by <nostr-name pubkey="`+wikiOther+`"`, `| pending</wiki-proposal>`)311+ wantAll(t, path, body, `<wiki-proposal data-state="pending">Proposed by <nostr-name pubkey="`+wikiOther+`"`, `| pending<small>showing pending revision 2; readers see approved revision 1</small></wiki-proposal>`)249312 wantNone(t, path, body, "<nostr-react", ">Accept<", ">Reject<")250313 body = wikiGet(t, wikiApp(t, b, wikiOther), "/wiki")251314 wantAll(t, "/wiki", body, `<tr data-state="pending"><td><a href="/wiki/release-notes-1-4">Release notes 1.4 &lt;b&gt;</a> <small>proposed | pending</small></td>`)