| 1 | {{define "repos" }}<section> |
| 2 | <h1>Repositories</h1> |
| 3 | {{if .Error}}{{template "alert" .Error}} |
| 4 | {{else if .Feed}}<table id="rows" > |
| 5 | <thead><tr><th>owner</th><th>repository</th><th>head</th></tr></thead> |
| 6 | <tbody> |
| 7 | {{range .Feed}}{{$r := asMap .}}<tr><td>{{template "nostrKey" (str (index $r "owner" ))}}</td><td><a href="/repo?owner={{urlquery (str (index $r " owner"))}}&repo={{urlquery (str (index $r " identifier"))}}&view=home" >{{str (index $r "identifier" )}}</a>{{if index $r "private" }} <small>private</small>{{end}}</td><td>{{shortID (index $r "head" )}}</td></tr> |
| 8 | {{end}}</tbody> |
| 9 | </table> |
| 10 | {{else}}<p>No repositories were reported.</p> |
| 11 | {{end}}{{with (asMap .Event).next_cursor}}<p id="pages" ><a href="{{browsePageURL " /repos" $.Query " cursor" .}}" >next page</a></p> |
| 12 | {{end}}</section> |
| 13 | {{end}} |
| 14 | |
| 15 | {{define "files" }}{{$data := asMap .Event}}{{$view := or (str (index $data "view" )) (.Query.Get "view" ) "library" }}<section id="file-library" data-pubkey="{{.Actor}}" > |
| 16 | <h1>Files</h1> |
| 17 | <nav id="file-views" aria-label="File collections" ><a href="/files" {{if eq $view "library" }} aria-current="page" {{end}}>My files</a><a href="/files?view=sites" {{if eq $view "sites" }} aria-current="page" {{end}}>Sites</a><a href="/files?view=rooms" {{if eq $view "rooms" }} aria-current="page" {{end}}>Rooms</a>{{if index $data "can_manage" }}<a href="/files?view=storage" {{if eq $view "storage" }} aria-current="page" {{end}}>Storage</a>{{end}}</nav> |
| 18 | <p id="file-context" >{{if eq $view "sites" }}Published assets, organized by site.{{else if eq $view "rooms" }}Attachments from rooms you can access.{{else if eq $view "storage" }}All stored files, newest first.{{else}}Your uploads, folders and files shared with relay members.{{end}}</p> |
| 19 | <form method="get" action="/files" id="file-search" ><input type="hidden" name="view" value="{{$view}}" >{{with .Query.Get "path" }}<input type="hidden" name="path" value="{{.}}" >{{end}}<label>Search <input name="q" value="{{.Query.Get " q"}}" placeholder="Name, type or hash" autocomplete="off" ></label> <button>Search</button>{{if .Query.Get "q" }} <a href="{{browsePageURL " /files" .Query " q" " "}}" >Clear</a>{{end}}</form> |
| 20 | {{with index $data "breadcrumbs" }}<nav id="file-breadcrumbs" aria-label="Folder path" >{{range $i, $raw := .}}{{$crumb := asMap $raw}}{{if $i}} / {{end}}<a href="{{str (index $crumb " open")}}" >{{str (index $crumb "name" )}}</a>{{end}}</nav>{{end}} |
| 21 | {{if .Error}}{{template "alert" .Error}} |
| 22 | {{else if .Feed}}<table id="rows" > |
| 23 | <thead><tr><th scope="col" >Name</th><th scope="col" >Details</th><th scope="col" >Added</th></tr></thead> |
| 24 | <tbody> |
| 25 | {{range .Feed}}{{$f := asMap .}}{{$kind := str (index $f "kind" )}}{{$hash := str (index $f "sha256" )}}{{$name := or (str (index $f "name" )) (shortID $hash)}}{{$open := or (str (index $f "open" )) (printf "/file?hash=%s" (urlquery $hash))}}<tr data-kind="{{$kind}}" data-hash="{{$hash}}" ><td><a href="{{$open}}" >{{if and $hash (hasPrefix (str (index $f "type" )) "image/" )}}<img loading="lazy" src="/files/raw?hash={{urlquery $hash}}" alt="" >{{else}}<span data-file-icon aria-hidden="true" >{{if or (eq $kind "folder" ) (eq $kind "site" ) (eq $kind "room" )}}▱{{else}}▤{{end}}</span>{{end}}<span>{{$name}}</span></a>{{with index $f "context_url" }}<small><a href="{{.}}" >{{str (index $f "context" )}}</a></small>{{end}}</td><td>{{if eq $kind "folder" }}Folder{{else if eq $kind "site" }}Site{{else if eq $kind "room" }}Room{{else}}{{fileSize (index $f "size" )}}<small>{{str (index $f "type" )}}</small>{{if eq (str (index $f "access" )) "members" }}<small>Relay members</small>{{else if eq (str (index $f "purpose" )) "file" }}<small>Secret link</small>{{end}}{{end}}</td><td>{{with index $f "uploaded" }}<time datetime="{{datetime .}}" >{{when .}}</time>{{end}}</td></tr> |
| 26 | {{end}}</tbody> |
| 27 | </table> |
| 28 | {{else}}<p id="file-empty" >{{if .Query.Get "q" }}No files match this search.{{else if eq $view "sites" }}No site files here yet.{{else if eq $view "rooms" }}No room attachments here yet.{{else if .Query.Get "path" }}This folder is empty.{{else}}No files here yet. Use Upload to add files or a folder.{{end}}</p> |
| 29 | {{end}}{{with (asMap .Event).next_cursor}}<p id="pages" ><a href="{{browsePageURL " /files" $.Query " cursor" .}}" >next page</a></p> |
| 30 | {{end}}</section> |
| 31 | {{end}} |
| 32 | |
| 33 | {{define "file" }}{{if .Error}}<section><h1>File</h1>{{template "alert" .Error}}</section> |
| 34 | {{else}}{{$file := asMap .Event}}{{$hash := or (.Query.Get "hash" ) (.Query.Get "sha" )}}<section id="file-view" data-pubkey="{{.Actor}}" > |
| 35 | <h1>{{or (str (index $file "name" )) "File" }}</h1>{{if eq (str (index $file "access" )) "members" }}<p>Relay members can view and download this file.</p>{{end}} |
| 36 | |
| 37 | <file-workspace-root hash="{{str (index $file " sha256")}}" type="{{str (index $file " type")}}" hidden></file-workspace-root> |
| 38 | <noscript><p>Enable JavaScript to decrypt files and browse folder manifests locally. Raw downloads remain available above.</p></noscript> |
| 39 | |
| 40 | {{$type := str (index $file "type" )}}{{$raw := printf "/files/raw?hash=%s" (urlquery (str (index $file "sha256" )))}}{{if hasPrefix $type "image/" }}<div id="media" ><img src="{{$raw}}" alt="" ></div> |
| 41 | {{else if hasPrefix $type "video/" }}<div id="media" ><video controls src="{{$raw}}" ></video></div> |
| 42 | {{else if hasPrefix $type "audio/" }}<div id="media" ><audio controls src="{{$raw}}" ></audio></div> |
| 43 | {{else if eq $type "application/pdf" }}<div id="media" ><a fx-ignore href="{{$raw}}" >Open the PDF</a></div> |
| 44 | {{else if index $file "binary" }}<p id="media" >This is a binary file. Download it to view its contents.</p> |
| 45 | {{else if index $file "content" }}{{sourceHTML .Event}} |
| 46 | {{else}}<p id="media" >This file is empty.</p> |
| 47 | {{end}}<file-tools hash="{{str (index $file " sha256")}}" type="{{str (index $file " type")}}" size="{{str (index $file " size")}}" name="{{or (str (index $file " name")) (shortID (index $file " sha256"))}}" ></file-tools><details id="file-facts" ><summary>File details</summary><table id="facts" > |
| 48 | <tr><th>sha256</th><td><code>{{str (index $file "sha256" )}}</code></td></tr> |
| 49 | <tr><th>type</th><td>{{str (index $file "type" )}}</td></tr> |
| 50 | <tr><th>size</th><td>{{fileSize (index $file "size" )}}</td></tr> |
| 51 | {{with index $file "blossom_uri" }}<tr><th>blossom</th><td><code>{{.}}</code></td></tr>{{end}} |
| 52 | <tr><th>download</th><td><a fx-ignore href="/files/raw?hash={{urlquery (str (index $file " sha256"))}}" >raw</a></td></tr> |
| 53 | </table></details>{{if .Actor}}<details id="manage-file" ><summary>Manage file</summary><rpc-form method="deleteblob" ><input type="hidden" name="param" value="{{$hash}}" ><button>Delete file</button></rpc-form></details>{{end}}</section> |
| 54 | {{end}}{{end}} |
| 55 | |
| 56 | {{define "approvals" }}<section> |
| 57 | <h1>Approvals</h1> |
| 58 | <p>Requests that wait for your decision. Review requests, answer questions and approve proposed changes.</p> |
| 59 | {{if not .Actor}}<p><a href="{{signinURL .Base .Path .Query}}" >Sign in</a> to see the requests addressed to your key.</p> |
| 60 | {{else if .Error}}{{template "alert" .Error}} |
| 61 | {{else}}{{$open := approvalViews .Event .URL "open" }}{{$done := approvalViews .Event .URL "answered" "expired" }}{{if $open}}{{range $item := $open}}<approval-item id="approval-{{$item.ID}}" data-type="{{$item.Type}}" data-state="{{$item.State}}" > |
| 62 | <header>{{template "nostrKey" $item.Asker}} in {{$item.Where}} | <time datetime="{{datetime $item.CreatedAt}}" >{{when $item.CreatedAt}}</time>{{if $item.Expires}} | expires <time datetime="{{datetime $item.Expires}}" >{{when $item.Expires}}</time>{{end}}</header> |
| 63 | <p>{{$item.Title}}</p>{{with $item.Body}}<p>{{.}}</p>{{end}}{{with $item.About}}<pre>{{.}}</pre>{{end}} |
| 64 | {{with $item.Grant}}<agent-grant-request request="{{$item.ID}}" data-review="{{.Raw}}" {{if .Error}} data-stale="true" {{end}}><h4>Proposed grant change</h4><table><thead><tr><th>scope</th><th>current</th><th>proposed</th></tr></thead><tbody>{{range .Rows}}{{if ne .Before .After}}<tr><th>{{.Label}}</th><td>{{.Before}}</td><td>{{.After}}</td></tr>{{end}}{{end}}</tbody></table>{{with .Error}}<p role="alert" >{{.}}</p>{{else}}<button type="button" data-grant-approve disabled>Approve and grant</button>{{end}}<button type="button" data-grant-deny disabled>Deny</button></agent-grant-request>{{end}} |
| 65 | <footer>{{if and (ne $item.Type "question" ) (ne $item.Type "grant" )}}<nostr-react event="{{$item.ID}}" pubkey="{{$item.Asker}}" kind="{{$item.Kind}}" ><button name="reaction" value="+" >{{if eq $item.Type "wiki-proposal" }}Accept{{else if eq $item.Type "wiki-merge" }}Accept{{else}}Approve{{end}}</button> <button name="reaction" value="-" >{{if eq $item.Type "wiki-proposal" }}Reject{{else if eq $item.Type "wiki-merge" }}Reject{{else}}Deny{{end}}</button></nostr-react> {{end}}<details><summary>Reply</summary><nostr-compose kind="1111" root="{{$item.ID}}" root-pubkey="{{$item.Asker}}" root-kind="{{$item.Kind}}" {{with $item.Coordinate}} coordinate="{{.}}" {{end}}><label>Reply <textarea name="content" required></textarea></label><button>Sign and reply</button></nostr-compose></details> <a href="{{$item.Open}}" >open</a></footer> |
| 66 | </approval-item> |
| 67 | {{end}}{{else}}<p>Nothing waits for your decision.</p> |
| 68 | {{end}}{{if $done}}<h3>Answered</h3> |
| 69 | <table id="rows" > |
| 70 | <thead><tr><th>asker</th><th>request</th><th>answer</th></tr></thead> |
| 71 | <tbody> |
| 72 | {{range $item := $done}}<tr id="approval-{{$item.ID}}" data-state="{{$item.State}}" ><td>{{template "nostrName" $item.Asker}}</td><td><a href="{{$item.Open}}" >{{$item.Title}}</a>{{if eq $item.Decision "replied" }}{{with $item.AnswerContent}} <small>{{.}}</small>{{end}}{{end}}</td><td>{{if eq $item.State "expired" }}expired{{else}}{{$item.Decision}} | <time datetime="{{datetime $item.AnsweredAt}}" >{{when $item.AnsweredAt}}</time>{{end}}</td></tr> |
| 73 | {{end}}</tbody> |
| 74 | </table> |
| 75 | {{end}}{{with (asMap .Event).next_cursor}}<p id="pages" ><a href="{{browsePageURL " /approvals" $.Query " cursor" .}}" >next page</a></p> |
| 76 | {{end}}<noscript><p>Approve and Deny need JavaScript and a connected signer.</p></noscript>{{end}}</section> |
| 77 | {{end}} |
| 78 | |
| 79 | {{define "sites" }}<section> |
| 80 | <h1>Sites</h1> |
| 81 | <p>Static sites published to this relay as signed manifests. Each site is served on its own host.</p> |
| 82 | {{if .Error}}{{template "alert" .Error}} |
| 83 | {{else if .Feed}}<table id="rows" > |
| 84 | <thead><tr><th>author</th><th>site</th><th>updated</th></tr></thead> |
| 85 | <tbody> |
| 86 | {{range .Feed}}{{$s := asMap .}}<tr><td>{{template "nostrKey" (str (index $s "author" ))}}</td><td><a href="{{str (index $s " url")}}" >{{str (index $s "label" )}}</a> <small>{{str (index $s "paths" )}} paths</small></td><td>{{when (index $s "created_at" )}}</td></tr> |
| 87 | {{end}}</tbody> |
| 88 | </table> |
| 89 | {{else}}<p>No sites are published.</p> |
| 90 | {{end}}</section> |
| 91 | {{end}} |
| 92 | |
| 93 | {{define "profile" }}{{$data := asMap .Event}}{{$p := asMap (index $data "profile" )}}<section> |
| 94 | <h1>Profile</h1> |
| 95 | <p>Your profile is a kind 0 event signed by your key. Saving publishes it to this relay first, then to the relays you list, so apps and people everywhere see the same name.</p> |
| 96 | {{template "alert" .Error}} |
| 97 | <profile-form pubkey="{{.Actor}}" existing="{{json (index $data " profile")}}" > |
| 98 | <label>Name <input name="name" maxlength="64" value="{{str (index $p " name")}}" placeholder="dami" ></label> |
| 99 | <label>Display name <input name="display_name" maxlength="64" value="{{str (index $p " display_name")}}" ></label> |
| 100 | <label>About <textarea name="about" maxlength="2000" >{{str (index $p "about" )}}</textarea></label> |
| 101 | <label>Picture <input name="picture" type="url" value="{{str (index $p " picture")}}" placeholder="https://" ></label> |
| 102 | <label>Banner <input name="banner" type="url" value="{{str (index $p " banner")}}" placeholder="https://" ></label> |
| 103 | <label>Website <input name="website" type="url" value="{{str (index $p " website")}}" placeholder="https://" ></label> |
| 104 | <label>NIP-05 <input name="nip05" value="{{str (index $p " nip05")}}" placeholder="you@example.com" ></label> |
| 105 | <label>Lightning address <input name="lud16" value="{{str (index $p " lud16")}}" placeholder="you@wallet.example" ></label> |
| 106 | <label>Also publish to <textarea name="relays" placeholder="wss://relay.example" >{{range index $data "relays" }}{{.}} |
| 107 | {{end}}</textarea></label> |
| 108 | <button>Sign and publish</button> |
| 109 | <noscript><p>Saving needs JavaScript and a connected signer. You can also publish a kind 0 event from any client with this relay in its list.</p></noscript> |
| 110 | </profile-form> |
| 111 | {{with index $data "event" }}{{$e := asMap .}}<p><small>Current profile published <time datetime="{{datetime (index $e " created_at")}}" >{{when (index $e "created_at" )}}</time>; fields this form does not show are kept as they are.</small></p>{{else}}<p><small>This relay holds no profile for your key yet.</small></p>{{end}} |
| 112 | </section> |
| 113 | {{end}} |