clone

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

feat/ui-updates
tinyrelay / tinyclient / browse.html
browse.html 12.1K
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="{{repoHref (or (str (index $r "handle")) (str (index $r "owner"))) (str (index $r "identifier")) "home" ""}}">{{str (index $r "identifier")}}</a>{{if index $r "private"}} <small>private</small>{{end}}</td><td>{{refName (index $r "head")}}</td></tr>8{{end}}</tbody>9</table>10{{else}}<p data-empty>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}}1415{{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{{if not (authRequired .Error)}}<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/sites"{{if eq $view "sites"}} aria-current="page"{{end}}>Sites</a><a href="/files/rooms"{{if eq $view "rooms"}} aria-current="page"{{end}}>Rooms</a>{{if index $data "can_manage"}}<a href="/files/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.{{end}}</p>19<form method="get" action="{{.Path}}" id="file-search">{{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 .Path .Query "q" ""}}">Clear</a>{{end}}</form>{{end}}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/%s" $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 $.Path $.Query "cursor" .}}">next page</a></p>30{{end}}</section>31{{end}}3233{{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}}3637<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>3940{{$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 data-danger>Delete file</button></rpc-form></details>{{end}}</section>54{{end}}{{end}}5556{{define "approvals"}}<section id="approvals">57<h1>Approvals</h1>58{{if not .Actor}}<p><a href="{{signinURL .Base .Path .Query}}">Sign in</a> to see the requests addressed to your key.</p>59{{else if .Error}}{{template "alert" .Error}}60{{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}}">61<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>62<p>{{$item.Title}}</p>{{with $item.Body}}<p>{{.}}</p>{{end}}{{with $item.About}}<pre>{{.}}</pre>{{end}}63{{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 data-primary disabled>Approve and grant</button>{{end}}<button type="button" data-grant-deny data-danger disabled>Deny</button></agent-grant-request>{{end}}64<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="+" data-primary>{{if eq $item.Type "wiki-proposal"}}Accept{{else if eq $item.Type "wiki-merge"}}Accept{{else}}Approve{{end}}</button> <button name="reaction" value="-" data-danger>{{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>65</approval-item>66{{end}}{{else}}<p data-empty>Nothing waits for your decision.</p>67{{end}}{{if $done}}<h3>Answered</h3>68<table id="rows">69<thead><tr><th>asker</th><th>request</th><th>answer</th></tr></thead>70<tbody>71{{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"}}<status-badge kind="expired">expired</status-badge>{{else}}<status-badge kind="{{$item.Decision}}">{{$item.Decision}}</status-badge> <time datetime="{{datetime $item.AnsweredAt}}">{{when $item.AnsweredAt}}</time>{{end}}</td></tr>72{{end}}</tbody>73</table>74{{end}}{{with (asMap .Event).next_cursor}}<p id="pages"><a href="{{browsePageURL "/approvals" $.Query "cursor" .}}">next page</a></p>75{{end}}<noscript><p>Approve and Deny need JavaScript and a connected signer.</p></noscript>{{end}}</section>76{{end}}7778{{define "sites"}}<section>79<h1>Sites</h1>80{{if .Error}}{{template "alert" .Error}}81{{else if .Feed}}<table id="rows">82<thead><tr><th>author</th><th>site</th><th>updated</th></tr></thead>83<tbody>84{{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>85{{end}}</tbody>86</table>87{{else}}<p data-empty>No sites are published.</p>88{{end}}</section>89{{end}}9091{{define "profile"}}{{$data := asMap .Event}}{{$p := asMap (index $data "profile")}}<section>92<h1>Profile</h1>93<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>94{{template "alert" .Error}}95<profile-form pubkey="{{.Actor}}" existing="{{json (index $data "profile")}}">96<label>Name <input name="name" maxlength="64" value="{{str (index $p "name")}}" placeholder="dami"></label>97<label>Display name <input name="display_name" maxlength="64" value="{{str (index $p "display_name")}}"></label>98<label>About <textarea name="about" maxlength="2000">{{str (index $p "about")}}</textarea></label>99<label>Picture <input name="picture" type="url" value="{{str (index $p "picture")}}" placeholder="https://"></label>100<label>Banner <input name="banner" type="url" value="{{str (index $p "banner")}}" placeholder="https://"></label>101<label>Website <input name="website" type="url" value="{{str (index $p "website")}}" placeholder="https://"></label>102<label>NIP-05 <input name="nip05" value="{{str (index $p "nip05")}}" placeholder="you@example.com"></label>103<label>Lightning address <input name="lud16" value="{{str (index $p "lud16")}}" placeholder="you@wallet.example"></label>104<label>Also publish to <textarea name="relays" placeholder="wss://relay.example">{{range index $data "relays"}}{{.}}105{{end}}</textarea></label>106<button data-primary>Sign and publish</button>107<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>108</profile-form>109{{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}}110</section>111{{end}}