| 1 | diff --git a/internal/webui/style.css b/internal/webui/style.css |
| 2 | index 5f1bb31..5d0c630 100644 |
| 3 | --- a/internal/webui/style.css |
| 4 | +++ b/internal/webui/style.css |
| 5 | @@ -230,7 +230,7 @@ merge-request,wiki-proposal,collaboration-proposal{display:block;border:1px soli |
| 6 | 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)} |
| 7 | 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)} |
| 8 | #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)} |
| 9 | -#merge-status{font:12px var(--mono);color:var(--muted)}#wiki-compare h2{margin-top:18px}#wiki-redirects{font:12px var(--mono);color:var(--muted)} |
| 10 | +#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)} |
| 11 | wiki-compose{display:block;max-width:720px;border-bottom:1px solid var(--line);padding:10px 0} |
| 12 | 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} |
| 13 | wiki-compose label:has(textarea){display:block}wiki-compose textarea{min-height:14em}wiki-compose button{margin-right:6px} |
| 14 | diff --git a/internal/webui/wiki.go b/internal/webui/wiki.go |
| 15 | index eab4f49..d438c68 100644 |
| 16 | --- a/internal/webui/wiki.go |
| 17 | +++ b/internal/webui/wiki.go |
| 18 | @@ -13,23 +13,37 @@ import ( |
| 19 | |
| 20 | // wikiView is one page as the templates see it. Version is the version |
| 21 | // being shown, Number its place counted from the oldest, Others the rest. |
| 22 | +// Revision and Revisions number the shown version among its author's |
| 23 | +// revisions; History lists every revision the viewer may see, newest |
| 24 | +// first. Archived is set when an older revision was opened by id, and |
| 25 | +// ReadersRevision names the approved revision readers see while the |
| 26 | +// shown one is a proposal that is not approved. |
| 27 | type wikiView struct { |
| 28 | - D, Title string |
| 29 | - Version map[string]any |
| 30 | - Author string |
| 31 | - Number, Count int |
| 32 | - Forks, Links int |
| 33 | - Versions []map[string]any |
| 34 | - Others []map[string]any |
| 35 | - Merges []map[string]any |
| 36 | - OpenMerges []map[string]any |
| 37 | - OpenCount int |
| 38 | - RedirectsTo []map[string]any |
| 39 | - RedirectsFrom []map[string]any |
| 40 | + D, Title string |
| 41 | + Version map[string]any |
| 42 | + Author string |
| 43 | + Number, Count int |
| 44 | + Revision, Revisions int |
| 45 | + Forks, Links int |
| 46 | + Versions []map[string]any |
| 47 | + Others []map[string]any |
| 48 | + History []map[string]any |
| 49 | + Archived bool |
| 50 | + ReadersRevision int |
| 51 | + Merges []map[string]any |
| 52 | + OpenMerges []map[string]any |
| 53 | + OpenCount int |
| 54 | + RedirectsTo []map[string]any |
| 55 | + RedirectsFrom []map[string]any |
| 56 | // CanApprove is set when the viewer may accept or reject proposals. |
| 57 | CanApprove bool |
| 58 | } |
| 59 | |
| 60 | +func wikiInt(value any) int { |
| 61 | + number, _ := value.(float64) |
| 62 | + return int(number) |
| 63 | +} |
| 64 | + |
| 65 | // wikiProposalView is how a version's proposal state reads for one viewer. |
| 66 | // Shown is set for the people who may see the state: the owner and |
| 67 | // moderators, and the version's author. Decide is set when the viewer may |
| 68 | @@ -77,14 +91,25 @@ func wikiPageView(result any) wikiView { |
| 69 | view.Merges = wikiMaps(page["merges"]) |
| 70 | view.RedirectsTo = wikiMaps(page["redirects_to"]) |
| 71 | view.RedirectsFrom = wikiMaps(page["redirects_from"]) |
| 72 | + view.History = wikiMaps(page["history"]) |
| 73 | view.CanApprove, _ = page["can_approve"].(bool) |
| 74 | view.Count = len(view.Versions) |
| 75 | if current := valueMap(page["version"]); current != nil { |
| 76 | view.Version = current |
| 77 | view.Author = plainString(current["author"]) |
| 78 | + view.Revision, view.Revisions = wikiInt(current["revision"]), wikiInt(current["revisions"]) |
| 79 | + view.Archived = plainString(current["superseded_by"]) != "" && plainString(page["preferred_by"]) == "version" |
| 80 | if links, ok := current["links"].([]any); ok { |
| 81 | view.Links = len(links) |
| 82 | } |
| 83 | + if current["proposal"] == true && plainString(current["approval"]) != "approved" { |
| 84 | + for _, h := range view.History { |
| 85 | + if plainString(h["author"]) == view.Author && plainString(h["approval"]) == "approved" { |
| 86 | + view.ReadersRevision = wikiInt(h["revision"]) |
| 87 | + break |
| 88 | + } |
| 89 | + } |
| 90 | + } |
| 91 | } |
| 92 | if view.Title == "" { |
| 93 | view.Title = view.D |
| 94 | diff --git a/internal/webui/wiki.html b/internal/webui/wiki.html |
| 95 | index ac97c2e..d567c17 100644 |
| 96 | --- a/internal/webui/wiki.html |
| 97 | +++ b/internal/webui/wiki.html |
| 98 | @@ -23,13 +23,14 @@ |
| 99 | <h1>{{$w.Title}}</h1> |
| 100 | {{template "alert" .Notice}}{{if not $w.Version}}<p>This name has no article of its own.</p> |
| 101 | {{template "wikiRedirects" $w}}{{if .Actor}}{{template "wikiEditor" .}}{{end}} |
| 102 | -{{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> |
| 103 | +{{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> |
| 104 | {{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> |
| 105 | {{end}}{{if .Query.Get "merge"}}{{template "wikiCompare" .}} |
| 106 | {{else if .Query.Get "edit"}}{{template "wikiEditor" .}} |
| 107 | {{else if .Query.Get "history"}}{{template "wikiHistory" .}} |
| 108 | -{{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> |
| 109 | -{{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> |
| 110 | +{{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> |
| 111 | +{{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> |
| 112 | +{{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> |
| 113 | {{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> |
| 114 | {{end}}{{wikiHTML (index $w.Version "content")}}</wiki-article> |
| 115 | {{template "wikiRedirects" $w}}{{end}}{{end}}</section> |
| 116 | @@ -62,18 +63,18 @@ |
| 117 | {{else}}<p>The destination author has no version of this page.</p> |
| 118 | {{end}}<p><a href="{{wikiURL $w.D}}">back to the page</a></p></div>{{end}}{{end}} |
| 119 | |
| 120 | -{{/* wikiHistory lists every version of a page, newest first. */}} |
| 121 | +{{/* wikiHistory lists every revision of a page across authors, newest first. */}} |
| 122 | {{define "wikiHistory"}}{{$w := wikiView .Event}}<table id="rows"> |
| 123 | -<thead><tr><th>author</th><th>version</th><th>notes</th><th>published</th></tr></thead> |
| 124 | +<thead><tr><th>author</th><th>revision</th><th>version</th><th>notes</th><th>published</th></tr></thead> |
| 125 | <tbody> |
| 126 | -{{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> |
| 127 | +{{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> |
| 128 | {{end}}</tbody> |
| 129 | </table> |
| 130 | <p><a href="{{wikiURL $w.D}}">back to the page</a></p>{{end}} |
| 131 | |
| 132 | {{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> |
| 133 | <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> |
| 134 | -{{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> |
| 135 | +{{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> |
| 136 | {{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> |
| 137 | {{end}}{{if $w.RedirectsTo}}<section><h4>Also known as</h4><ul>{{range $w.RedirectsTo}}<li><code>{{str (index . "d")}}</code></li>{{end}}</ul></section> |
| 138 | {{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}} |
| 139 | diff --git a/internal/webui/wiki_test.go b/internal/webui/wiki_test.go |
| 140 | index fa3eb3b..630305b 100644 |
| 141 | --- a/internal/webui/wiki_test.go |
| 142 | +++ b/internal/webui/wiki_test.go |
| 143 | @@ -18,6 +18,10 @@ const ( |
| 144 | wikiMerge = "3333333333333333333333333333333333333333333333333333333333333333" |
| 145 | wikiOwnerV = "4444444444444444444444444444444444444444444444444444444444444444" |
| 146 | wikiOtherV = "5555555555555555555555555555555555555555555555555555555555555555" |
| 147 | + // wikiOwnerOld and wikiOtherOld are archived revisions that the |
| 148 | + // current versions replaced. |
| 149 | + wikiOwnerOld = "7777777777777777777777777777777777777777777777777777777777777777" |
| 150 | + wikiOtherOld = "8888888888888888888888888888888888888888888888888888888888888888" |
| 151 | ) |
| 152 | |
| 153 | // wikiBackend answers the three wiki queries with one page that has two |
| 154 | @@ -29,11 +33,15 @@ type wikiBackend struct { |
| 155 | private bool |
| 156 | // proposal makes the second author's version a proposal in this state |
| 157 | // (pending, approved or rejected), decided by the owner when it is not |
| 158 | - // pending. approver marks the caller as allowed to decide, and shown |
| 159 | - // makes the proposal the page's shown version. |
| 160 | + // pending, with an approved earlier revision behind it in the history. |
| 161 | + // approver marks the caller as allowed to decide, and shown makes the |
| 162 | + // proposal the page's shown version. |
| 163 | proposal string |
| 164 | approver bool |
| 165 | shown bool |
| 166 | + // archived gives the owner's version an earlier revision, which a |
| 167 | + // request naming it by version opens. |
| 168 | + archived bool |
| 169 | } |
| 170 | |
| 171 | func (b *wikiBackend) Query(_ context.Context, method string, params []json.RawMessage, actor string) (any, error) { |
| 172 | @@ -49,20 +57,31 @@ func (b *wikiBackend) Query(_ context.Context, method string, params []json.RawM |
| 173 | if b.private && actor == "" { |
| 174 | return nil, errors.New("auth-required: sign in to read this relay") |
| 175 | } |
| 176 | - 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), |
| 177 | + 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), |
| 178 | "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"}} |
| 179 | - 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."} |
| 180 | + 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."} |
| 181 | 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"} |
| 182 | - shown := owner |
| 183 | + shown, preferredBy := owner, "owner" |
| 184 | + history := []any{other, owner} |
| 185 | if b.proposal != "" { |
| 186 | - other["proposal"], other["approval"] = true, b.proposal |
| 187 | + other["proposal"], other["approval"], other["revision"], other["revisions"] = true, b.proposal, float64(2), float64(2) |
| 188 | if b.proposal != "pending" { |
| 189 | other["approval_event"], other["approval_at"], other["approval_by"] = wikiMerge, float64(1788882000), wikiOwner |
| 190 | } |
| 191 | + 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} |
| 192 | + history = []any{other, old, owner} |
| 193 | if b.shown { |
| 194 | shown = other |
| 195 | } |
| 196 | } |
| 197 | + if b.archived { |
| 198 | + owner["revision"], owner["revisions"] = float64(2), float64(2) |
| 199 | + 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."} |
| 200 | + history = append(history, old) |
| 201 | + if object["version"] == wikiOwnerOld { |
| 202 | + shown, preferredBy = old, "version" |
| 203 | + } |
| 204 | + } |
| 205 | switch method { |
| 206 | case "browsewiki": |
| 207 | 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 |
| 208 | @@ -70,7 +89,7 @@ func (b *wikiBackend) Query(_ context.Context, method string, params []json.RawM |
| 209 | if object["d"] != "release-notes-1-4" { |
| 210 | return nil, errors.New("not found: wiki page") |
| 211 | } |
| 212 | - 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}, nil |
| 213 | + 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}, nil |
| 214 | case "browsewikimerge": |
| 215 | return map[string]any{"merge": merge, "proposed": other, "target": owner}, nil |
| 216 | } |
| 217 | @@ -151,7 +170,7 @@ func TestWikiPageRendersArticleMergeBarAndPanel(t *testing.T) { |
| 218 | `<merge-request><nostr-name pubkey="`+wikiOther+`"`, `proposes a new version of this page`, `href="/wiki/release-notes-1-4?merge=`+wikiMerge+`">compare</a>`, |
| 219 | `<nostr-react event="`+wikiMerge+`" pubkey="`+wikiOther+`" kind="818"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react>`, |
| 220 | `<a href="/e/`+wikiMerge+`">Reply</a>`, |
| 221 | - `<wiki-article><header>wiki | release-notes-1-4 | by <nostr-name pubkey="`+wikiOwner+`"`, `version 1 of 2 | 1 fork</header>`, |
| 222 | + `<wiki-article><header>wiki | release-notes-1-4 | revision 1 by <nostr-name pubkey="`+wikiOwner+`"`, `version 1 of 2 | 1 fork</header>`, |
| 223 | `<h1>Highlights</h1>`, `<em>encrypted</em>`, `<a href="/wiki/files-and-private-repositories">Files and Private Repositories</a>`, `<script>alert(1)</script>`, |
| 224 | `<h4>Page</h4>`, `<th>versions</th><td>2</td>`, `<th>merge requests</th><td>1 open</td>`, `<th>links</th><td>1</td>`, |
| 225 | `<h4>Other versions</h4>`, `href="/wiki/release-notes-1-4?version=`+wikiOtherV+`">222222222222</a> | fork`, |
| 226 | @@ -186,7 +205,44 @@ func TestWikiPageCompareEditorAndHistory(t *testing.T) { |
| 227 | |
| 228 | path = "/wiki/release-notes-1-4?history=1" |
| 229 | body = wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 230 | - wantAll(t, path, body, `<th>author</th><th>version</th>`, `fork of 444444444444`, `href="/wiki/release-notes-1-4?version=`+wikiOwnerV+`">444444444444</a>`) |
| 231 | + 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>`) |
| 232 | + wantAll(t, path, body, `<th>revisions</th><td>2</td>`) |
| 233 | + wantNone(t, path, body, "<small>replaced</small>") |
| 234 | +} |
| 235 | + |
| 236 | +func TestWikiHistoryListsRevisionsWithStates(t *testing.T) { |
| 237 | + b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, proposal: "pending", approver: true, shown: true, archived: true} |
| 238 | + path := "/wiki/release-notes-1-4?history=1" |
| 239 | + body := wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 240 | + wantAll(t, path, body, |
| 241 | + `<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>`, |
| 242 | + `<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>`, |
| 243 | + `<td>2 <small>current</small></td><td><a href="/wiki/release-notes-1-4?version=`+wikiOwnerV+`">444444444444</a></td>`, |
| 244 | + `<td>1 <small>replaced</small></td><td><a href="/wiki/release-notes-1-4?version=`+wikiOwnerOld+`">777777777777</a></td><td> | 1 likes</td>`, |
| 245 | + `<th>revisions</th><td>4</td>`) |
| 246 | + // A member sees the rows without their proposal states. |
| 247 | + b.approver = false |
| 248 | + body = wikiGet(t, wikiApp(t, b, wikiMember), path) |
| 249 | + 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>`) |
| 250 | + wantNone(t, path, body, "<tr data-state=", "proposed ") |
| 251 | +} |
| 252 | + |
| 253 | +func TestWikiArchivedRevisionShowsANote(t *testing.T) { |
| 254 | + b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, archived: true} |
| 255 | + path := "/wiki/release-notes-1-4?version=" + wikiOwnerOld |
| 256 | + body := wikiGet(t, wikiApp(t, b, wikiOther), path) |
| 257 | + if b.params["browsewikipage"]["version"] != wikiOwnerOld { |
| 258 | + t.Fatalf("version lookup params=%v", b.params["browsewikipage"]) |
| 259 | + } |
| 260 | + wantAll(t, path, body, |
| 261 | + `<wiki-article><header>wiki | release-notes-1-4 | revision 1 by <nostr-name pubkey="`+wikiOwner+`"`, |
| 262 | + `<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>`, |
| 263 | + `The first draft.`) |
| 264 | + // The author's current revision carries no note. |
| 265 | + path = "/wiki/release-notes-1-4" |
| 266 | + body = wikiGet(t, wikiApp(t, b, wikiOther), path) |
| 267 | + wantAll(t, path, body, `revision 2 by <nostr-name pubkey="`+wikiOwner+`"`, `<em>encrypted</em>`) |
| 268 | + wantNone(t, path, body, `id="wiki-revision-note"`, "The first draft.") |
| 269 | } |
| 270 | |
| 271 | func TestWikiMissingPageOffersTheEditorToMembers(t *testing.T) { |
| 272 | @@ -221,15 +277,22 @@ func TestWikiProposalBarOffersTheDecisionToApprovers(t *testing.T) { |
| 273 | path := "/wiki/release-notes-1-4" |
| 274 | body := wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 275 | wantAll(t, path, body, |
| 276 | - `<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>`, |
| 277 | - `<wiki-article><header>wiki | release-notes-1-4 | by <nostr-name pubkey="`+wikiOther+`"`, `A proposed rewrite.`) |
| 278 | + `<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>`, |
| 279 | + `<wiki-article><header>wiki | release-notes-1-4 | revision 2 by <nostr-name pubkey="`+wikiOther+`"`, `A proposed rewrite.`) |
| 280 | // A decided proposal names the decision, its time and the deciding key, |
| 281 | - // and offers no buttons. |
| 282 | + // and offers no buttons. Readers still see the earlier approved |
| 283 | + // revision while the edit is rejected, and the edit itself once it is |
| 284 | + // approved. |
| 285 | for _, state := range []string{"approved", "rejected"} { |
| 286 | b.proposal = state |
| 287 | body = wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 288 | 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+`"`) |
| 289 | wantNone(t, path, body, "<nostr-react", ">Accept<") |
| 290 | + if state == "rejected" { |
| 291 | + wantAll(t, path, body, `<small>showing rejected revision 2; readers see approved revision 1</small></wiki-proposal>`) |
| 292 | + } else { |
| 293 | + wantNone(t, path, body, "readers see approved revision") |
| 294 | + } |
| 295 | } |
| 296 | // A pending proposal that is not the shown version is marked in the |
| 297 | // panel and the history, with no bar above the article. |
| 298 | @@ -245,7 +308,7 @@ func TestWikiProposalAuthorSeesTheStateWithoutButtons(t *testing.T) { |
| 299 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, proposal: "pending", shown: true} |
| 300 | path := "/wiki/release-notes-1-4" |
| 301 | body := wikiGet(t, wikiApp(t, b, wikiOther), path) |
| 302 | - wantAll(t, path, body, `<wiki-proposal data-state="pending">Proposed by <nostr-name pubkey="`+wikiOther+`"`, `| pending</wiki-proposal>`) |
| 303 | + 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>`) |
| 304 | wantNone(t, path, body, "<nostr-react", ">Accept<", ">Reject<") |
| 305 | body = wikiGet(t, wikiApp(t, b, wikiOther), "/wiki") |
| 306 | wantAll(t, "/wiki", body, `<tr data-state="pending"><td><a href="/wiki/release-notes-1-4">Release notes 1.4 <b></a> <small>proposed | pending</small></td>`) |