1{{/* The page shell owns the rail, content column, contextual panel and2footer. Page templates render one section selected by .Tab. The body works3before scripts load; tiny.js and bridge.js establish shared browser services,4then feature modules are loaded for the current page. */}}5{{define "page"}}6<html lang="en" data-js="no" data-scripts="{{scriptsVersion}}">7<head>8<meta charset="utf-8">9<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">10<meta name="color-scheme" content="light dark">11<meta name="theme-color" content="#f4f2ec" media="(prefers-color-scheme: light)">12<meta name="theme-color" content="#1e1d1a" media="(prefers-color-scheme: dark)">13<meta name="mobile-web-app-capable" content="yes">14<meta name="apple-mobile-web-app-capable" content="yes">15<meta name="apple-mobile-web-app-status-bar-style" content="default">16<meta name="apple-mobile-web-app-title" content="{{.Slug}}">17<title>{{.Title}}</title>18{{if or (and (eq .Tab "social") (eq (socialView .Query).Key "podcasts")) (eq .Tab "social-profile")}}<link rel="alternate" type="application/rss+xml" title="Podcasts" href="{{podcastFeedURL .Query}}">{{end}}19<link rel="manifest" href="/manifest.webmanifest">20<link rel="icon" href="/icon.svg" type="image/svg+xml">21<link rel="apple-touch-icon" href="/apple-touch-icon.png">22<style>{{stylesheet}}</style>23<script src="/fixi.js"></script>24<script src="/signer.js"></script>25<script src="/webmcp.js"></script>26</head>27<body>28{{$mobile := mobileHeader .}}<header id="topbar">29{{if $mobile.BackURL}}<a id="mobile-back" href="{{$mobile.BackURL}}" aria-label="{{$mobile.BackLabel}}" title="{{$mobile.BackLabel}}"><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m14 6-6 6 6 6"/></svg></a>{{end}}30<details id="context-menu" name="mobile-chrome"><summary id="context-toggle" aria-controls="aside" title="Page details">31{{if $mobile.RoomID}}<room-avatar picture="{{$mobile.Picture}}" fallback="{{roomAvatarURL .URL $mobile.RoomID}}" aria-hidden="true"><img src="{{if $mobile.Picture}}{{$mobile.Picture}}{{else}}{{roomAvatarURL .URL $mobile.RoomID}}{{end}}" alt="" width="36" height="36" loading="lazy"></room-avatar>{{else if $mobile.PubKey}}<nostr-avatar pubkey="{{$mobile.PubKey}}" aria-hidden="true"><img src="{{avatarURL $mobile.PubKey}}" alt="" width="36" height="36" loading="lazy"></nostr-avatar>{{end}}32<span id="mobile-heading"><strong id="mobile-title" role="heading" aria-level="1">{{if $mobile.PubKey}}{{template "nostrName" $mobile.PubKey}}{{else}}{{$mobile.Title}}{{end}}</strong><small id="mobile-context">{{$mobile.Subtitle}}</small></span><svg data-chevron viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 10 4 4 4-4"/></svg>33</summary></details>34<details id="nav-menu" name="mobile-chrome"><summary id="menu" aria-controls="rail" aria-label="Menu" title="Menu"><svg data-menu-icon viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg><svg data-close-icon viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="m6 6 12 12M6 18 18 6"/></svg></summary></details>35{{if and .Actor (not .Private)}}<signer-connection actor="{{.Actor}}" hidden></signer-connection>{{end}}36</header>37<table id="layout" aria-label="{{.Slug}}"><tbody><tr>38<td id="rail"><div id="railbox">{{template "rail" .}}</div></td>39<td id="main"><div id="content" tabindex="-1">40{{if .Private}}{{template "signin" .}}41{{else if eq .Tab "home"}}{{template "home" .}}42{{else if eq .Tab "inbox"}}{{template "inbox" .}}43{{else if eq .Tab "outbox"}}{{template "outbox" .}}44{{else if eq .Tab "search"}}{{template "search" .}}45{{else if eq .Tab "social"}}{{template "social" .}}46{{else if eq .Tab "social-thread"}}{{template "social-thread" .}}47{{else if eq .Tab "social-profile"}}{{template "social-profile" .}}48{{else if eq .Tab "private"}}{{template "private" .}}49{{else if eq .Tab "chat"}}{{template "chat" .}}50{{else if eq .Tab "direct"}}{{template "direct" .}}51{{else if eq .Tab "media"}}{{template "media" .}}52{{else if eq .Tab "sites"}}{{template "sites" .}}53{{else if eq .Tab "marmot"}}{{template "marmot" .}}54{{else if eq .Tab "grasp"}}{{template "grasp" .}}55{{else if eq .Tab "terms"}}{{template "terms" .}}56{{else if eq .Tab "people"}}{{template "people" .}}57{{else if eq .Tab "agents"}}{{template "agents" .}}58{{else if eq .Tab "moderation"}}{{template "moderation" .}}59{{else if eq .Tab "rules"}}{{template "rules" .}}60{{else if eq .Tab "identity"}}{{template "identity" .}}61{{else if eq .Tab "signin"}}{{template "signin" .}}62{{else if eq .Tab "account"}}{{template "account" .}}63{{else if eq .Tab "connect"}}{{template "connect" .}}64{{else if eq .Tab "data"}}{{template "data" .}}65{{else if eq .Tab "sync"}}{{template "sync" .}}66{{else if eq .Tab "views"}}{{template "views" .}}67{{else if eq .Tab "health"}}{{template "health" .}}68{{else if eq .Tab "owner"}}{{template "owner" .}}69{{else if eq .Tab "manage-restricted"}}{{template "manage-restricted" .}}70{{else if eq .Tab "repos"}}{{template "repos" .}}71{{else if eq .Tab "repo"}}{{template "repo" .}}72{{else if eq .Tab "files"}}{{template "files" .}}73{{else if eq .Tab "wiki"}}{{template "wiki" .}}74{{else if eq .Tab "wikipage"}}{{template "wikipage" .}}75{{else if eq .Tab "file"}}{{template "file" .}}76{{else if eq .Tab "approvals"}}{{template "approvals" .}}77{{else if eq .Tab "profile"}}{{template "profile" .}}78{{else if eq .Tab "rooms"}}{{template "rooms" .}}79{{else if eq .Tab "room"}}{{template "room" .}}80{{else if eq .Tab "thread"}}{{template "thread" .}}81{{else if eq .Tab "event"}}{{template "event" .}}82{{else if eq .Tab "view"}}{{template "view" .}}83{{else if eq .Tab "invite"}}{{template "invite" .}}84{{else}}{{template "home" .}}85{{end}}</div></td>86<td id="aside"><div id="panel">{{template "panel" .}}</div></td>87</tr></tbody></table>88<footer id="footer"><table><tr><td><span id="prompt"><b>{{.Slug}}</b> » <page-link url="{{pageAddress .URL .Path .Query}}" title="Copy the page address">{{prompt .Path .Query}}</page-link></span></td><td><span id="who">{{if .Private}}private relay{{else if .Actor}}signed in as <a href="/account">{{template "nostrName" .Actor}}</a> (<button id="session-logout" type="button">sign out</button>){{else}}browsing as guest (<a href="{{signinURL .Base .Path .Query}}">Sign in</a>){{end}}</span> <span id="session-status" role="status"></span><span id="navigation-status" role="status" aria-live="polite"></span></td></tr></table><button id="install" type="button" hidden>install app</button></footer>89<script src="{{scriptURL "tiny.js"}}"></script>90<script src="{{scriptURL "bridge.js"}}"></script>91{{if or (eq .Tab "files") (eq .Tab "file")}}<script src="{{scriptURL "blossom-encryption.js"}}"></script>92<script src="{{scriptURL "file-catalog.js"}}"></script>93<script src="{{scriptURL "blossom-manifests.js"}}"></script>94<script src="{{scriptURL "blossom-upload.js"}}"></script>95<script src="{{scriptURL "file-workspace.js"}}"></script>96{{end}}<script src="{{scriptURL "nostr-name.js"}}"></script>97<script src="{{scriptURL "components.js"}}"></script>98<script src="{{scriptURL "rooms.js"}}"></script>99<script src="{{scriptURL "file-messages.js"}}"></script>100<script src="{{scriptURL "direct-messages.js"}}"></script>101<script src="{{scriptURL "chat-files.js"}}"></script>102<script src="{{scriptURL "chat.js"}}"></script>103<script src="{{scriptURL "chat-presence.js"}}"></script>104<script src="{{scriptURL "chat-activity.js"}}"></script>105<script src="{{scriptURL "grant-request.js"}}"></script>106{{if eq .Tab "connect"}}<script src="{{scriptURL "private-services.js"}}"></script>107{{end}}</body>108</html>109{{end}}110111{{/* The rail is the sitemap for the current context: the relay, one repository, or management. */}}112{{define "rail"}}{{$kind := railKind .Tab}}{{if .Private}}<header><span><a id="mark" href="/"><i>t</i><i>i</i><i>n</i><i>y</i></a></span></header>113<nav><a href="{{signinURL .Base .Path .Query}}" aria-current="page">/signin</a></nav>114<p>Private relay. Sign in with a Nostr signer to continue.</p>115{{else if eq $kind "repo"}}<header><span><a href="/">{{.Slug}}</a>/<a href="/repos" title="{{.Query.Get "owner"}}">{{shortID (npub (.Query.Get "owner"))}}</a>/<b>{{.Query.Get "repo"}}</b></span></header>116<nav>{{$v := repoView .Query}}<a href="{{repoURL .Query "home" ""}}"{{if eq $v "home"}} aria-current="page"{{end}}>/home</a><a href="{{repoURL .Query "tree" ""}}"{{if or (eq $v "tree") (eq $v "file")}} aria-current="page"{{end}}>/code</a><a href="{{repoURL .Query "history" ""}}"{{if or (eq $v "history") (eq $v "commit")}} aria-current="page"{{end}}>/history</a><a href="{{repoURL .Query "issues" ""}}"{{if or (eq $v "issues") (eq $v "issue")}} aria-current="page"{{end}}>/issues</a><a href="{{repoURL .Query "prs" ""}}"{{if or (eq $v "prs") (eq $v "pr")}} aria-current="page"{{end}}>/prs</a><a href="{{repoURL .Query "activity" ""}}"{{if eq $v "activity"}} aria-current="page"{{end}}>/activity</a></nav>117{{else if eq $kind "social"}}<header><span><a href="/">{{.Slug}}</a>/<a href="/social"><b>social</b></a></span></header>118<nav aria-label="Social">{{$view := socialView .Query}}{{range socialViews}}<a href="{{.URL}}"{{if and (eq $.Tab "social") (eq $view.Key .Key)}} aria-current="page"{{end}}>/{{.Key}}</a>{{end}}</nav>119<nav aria-label="Social profile"><a href="/social/profile"{{if eq .Tab "social-profile"}} aria-current="page"{{end}}>/profile</a></nav>120{{else if eq $kind "manage"}}<header><span><a href="/">{{.Slug}}</a>/<b>manage</b></span></header>121{{range $i, $group := navGroups manageItems}}<nav{{with navGroupLabel "manage" $i}} data-label="{{.}}"{{end}}>{{range $item := $group}}<a href="{{$item.Href}}"{{if eq $item.Tab $.Tab}} aria-current="page"{{end}}>{{$item.Label}}</a>{{end}}</nav>{{end}}122{{else if eq $kind "rooms"}}<header><span><a href="/">{{.Slug}}</a>/<a href="/chat"><b>chat</b></a></span></header>123<nav aria-label="Chat"><a href="/chat"{{if eq .Tab "chat"}} aria-current="page"{{end}}>All conversations</a></nav>124{{if .Actor}}<h4>Direct chats</h4><direct-chats actor="{{.Actor}}" relay="{{wsURL .URL}}" compact><p>Connect your signer to see direct chats.</p></direct-chats>125<p><a href="/chat#new-chat">+ new chat</a></p>{{end}}126<h4>Rooms</h4>127<nav id="room-list">{{range .Rooms}}{{$r := asMap .}}{{$id := str (index $r "id")}}{{$picture := socialMediaURL (str (index $r "picture"))}}<a href="/rooms/{{$id}}"{{if eq $id $.View}} aria-current="page"{{end}}><room-avatar picture="{{$picture}}" fallback="{{roomAvatarURL $.URL $id}}" aria-hidden="true"><img src="{{if $picture}}{{$picture}}{{else}}{{roomAvatarURL $.URL $id}}{{end}}" alt="" width="24" height="24" loading="lazy"></room-avatar>{{or (str (index $r "name")) $id}}{{with age (index $r "last_message_at")}} <small>{{.}}</small>{{end}}</a>{{else}}<a href="/chat">Browse rooms</a>{{end}}</nav>128{{if .Actor}}<p><a href="/chat#new-room">+ new room</a></p>{{end}}129{{else}}<header><span><a id="mark" href="/" title="{{.Slug}}"><i>t</i><i>i</i><i>n</i><i>y</i></a></span></header>130{{range $i, $group := navGroups relayItems}}<nav{{with navGroupLabel "relay" $i}} data-label="{{.}}"{{end}}>{{range $item := $group}}<a href="{{$item.Href}}"{{if eq $item.Tab $.Tab}} aria-current="page"{{end}}>{{$item.Label}}</a>{{end}}</nav>{{end}}{{if .Actor}}<p id="personal-nav"><a href="/account"{{if eq .Tab "account"}} aria-current="page"{{end}}>/account</a></p>{{end}}131{{end}}<div id="mobile-actions">{{if .Actor}}<a href="/account">Account</a>{{end}}<page-link url="{{pageAddress .URL .Path .Query}}" title="Copy the page address"><button type="button">Copy page link</button></page-link></div><p id="rail-tools">{{if eq $kind "repo"}}<a href="/repos">← all repositories</a>{{else if or (eq $kind "manage") (eq $kind "rooms") (eq $kind "social")}}<a href="/">← relay</a>{{end}}<button id="theme" type="button" aria-label="Switch theme">theme</button></p>{{end}}132133{{/* The panel holds facts and filters for the current page. */}}134{{define "panel"}}{{$kind := railKind .Tab}}{{if .Private}}{{template "panel-signin" .}}135{{else if eq .Tab "repo"}}{{template "panel-repo" .}}136{{else if eq .Tab "agents"}}{{template "panel-agents" .}}137{{else if eq $kind "manage"}}{{template "panel-manage" .}}138{{else if or (eq .Tab "chat") (eq .Tab "direct")}}{{template "panel-chat" .}}139{{else if eq $kind "rooms"}}{{template "panel-rooms" .}}140{{else if eq $kind "social"}}{{template "panel-social" .}}141{{else if or (eq .Tab "inbox") (eq .Tab "outbox") (eq .Tab "search") (eq .Tab "articles")}}{{template "panel-feed" .}}142{{else if eq .Tab "repos"}}{{template "panel-repos" .}}143{{else if or (eq .Tab "files") (eq .Tab "file")}}{{template "panel-files" .}}144{{else if eq .Tab "approvals"}}{{template "panel-approvals" .}}145{{else if or (eq .Tab "wiki") (eq .Tab "wikipage")}}{{template "panel-wiki" .}}146{{else if eq .Tab "signin"}}{{template "panel-signin" .}}147{{else if eq .Tab "account"}}{{template "panel-account" .}}148{{else}}{{template "panel-relay" .}}149{{end}}{{end}}150151{{define "panel-relay"}}<section><h4>About</h4><table><tr><th>identity</th><td>{{template "nostrKey" .Identity}}</td></tr><tr><th>reads</th><td>{{.Policy.Reads}}</td></tr><tr><th>writes</th><td>{{.Policy.Writes}}</td></tr><tr><th>search</th><td>{{.Policy.Features.Search}}</td></tr>{{if .Policy.Features.Grasp}}<tr><th>git</th><td>GRASP-01</td></tr>{{end}}{{if .Policy.Features.Files}}<tr><th>files</th><td>Blossom</td></tr>{{end}}</table></section>152<section><h4>Actions</h4><ul>{{if .Actor}}<li><a href="/profile">profile</a></li>{{else}}<li><a href="{{signinURL .Base .Path .Query}}">sign in</a></li>{{end}}<li><a href="/.well-known/nostr.json">NIP-05 names</a></li>{{if .Policy.Features.Pages}}<li><a href="/feed.xml">Atom feed</a></li>{{end}}<li><a href="/terms">terms</a></li></ul></section>153{{end}}154155{{define "panel-signin"}}<section><h4>Session</h4><table><tr><th>status</th><td>{{if .Actor}}signed in{{else}}guest{{end}}</td></tr><tr><th>method</th><td>NIP-98</td></tr></table></section>156<section><h4>After sign in</h4><ul><li>join chats</li><li>manage members and policy</li><li>upload files</li><li>push repositories</li></ul></section>{{end}}157158{{define "panel-account"}}<section><h4>Personal</h4><table><tr><th>identity</th><td>{{template "nostrKey" .Actor}}</td></tr><tr><th>profile</th><td><a href="/profile">edit</a></td></tr></table></section>{{end}}159160{{define "panel-repos"}}<section><h4>Hosting</h4><table><tr><th>server</th><td><code>{{.URL}}</code></td></tr><tr><th>protocol</th><td>GRASP-01</td></tr></table></section>{{end}}161162{{define "panel-files"}}<section><h4>Storage</h4><table><tr><th>protocol</th><td>Blossom</td></tr>{{if .Policy.Features.Files}}<tr><th>uploads</th><td>members</td></tr>{{end}}</table></section>163{{if and .Actor (not (restricted .Error))}}<section><h4>Upload</h4><file-upload pubkey="{{.Actor}}"></file-upload><details><summary>Import from URL</summary><file-mirror action="/mirror"></file-mirror></details><noscript><p>Uploads need JavaScript and a connected signer.</p></noscript></section>{{end}}{{end}}164165{{define "panel-approvals"}}{{$counts := approvalCounts .Event}}{{$data := asMap .Event}}{{if .Actor}}<section><h4>Waiting</h4><table><tr><th>open</th><td>{{index $counts "open"}}</td></tr><tr><th>oldest</th><td>{{with index $data "oldest_open"}}<time datetime="{{datetime .}}">{{when .}}</time>{{else}}none{{end}}</td></tr><tr><th>answered</th><td>{{index $counts "answered"}}</td></tr><tr><th>expired</th><td>{{index $counts "expired"}}</td></tr></table></section>{{end}}166<section><h4>Devices</h4>{{if .Actor}}{{range $index, $device := approvalDevices .Event}}<p><i></i>device {{add $index 1}} | {{$device}}</p>{{else}}<p>No device receives notifications for this key. Enable them in Chat.</p>{{end}}{{else}}<p>Sign in to see which devices are notified.</p>{{end}}</section>{{end}}167168{{define "panel-feed"}}<section><h4>Filters</h4><form method="get" action="/{{.Tab}}"><label for="filter-kinds">kinds</label><select id="filter-kinds" name="kinds"><option value="">notes and articles</option><option value="1"{{if eq (.Query.Get "kinds") "1"}} selected{{end}}>1 notes</option><option value="30023"{{if eq (.Query.Get "kinds") "30023"}} selected{{end}}>30023 articles</option><option value="7"{{if eq (.Query.Get "kinds") "7"}} selected{{end}}>7 reactions</option><option value="all"{{if eq (.Query.Get "kinds") "all"}} selected{{end}}>everything</option></select><label for="filter-author">author</label><input id="filter-author" name="author" placeholder="64 hex pubkey" value="{{.Query.Get "author"}}"><label for="filter-since">since</label><input id="filter-since" name="since" type="date" value="{{.Query.Get "since"}}">{{if eq .Tab "search"}}<label for="filter-q">search</label><input id="filter-q" name="q" value="{{.Query.Get "q"}}">{{end}}<button>Apply</button></form></section>169{{if .Actor}}<section><h4>You</h4><table><tr><th>key</th><td>{{template "nostrKey" .Actor}}</td></tr></table></section>{{end}}170{{if and .Actor (eq .Tab "inbox")}}<section><h4>Notifications</h4><push-toggle></push-toggle><p><small>Relay notices for this key, on this device only.</small></p>{{if .Owner}}<rpc-form method="notifytest"><button>Send a test</button></rpc-form>{{end}}<noscript><p>Enabling notifications needs JavaScript.</p></noscript></section>{{end}}{{end}}171172{{define "panel-repo"}}{{$r := asMap .Event}}{{with index $r "repository"}}{{$r = asMap .}}{{end}}<section><h4>About</h4><table><tr><th>owner</th><td>{{template "nostrKey" (str (index $r "owner"))}}</td></tr><tr><th>visibility</th><td>{{if index $r "private"}}private{{else}}public{{end}}</td></tr>{{with index $r "commit"}}<tr><th>head</th><td><code title="{{.}}">{{shortID .}}</code></td></tr>{{end}}</table></section>173{{with index $r "maintainers"}}<section><h4>Maintainers</h4><ul id="maintainers">{{range .}}{{$m := asMap .}}<li>{{template "nostrName" (str (index $m "pubkey"))}}{{$role := str (index $m "role")}}{{if eq $role "agent"}} <small>agent{{with str (index $m "name")}} {{.}}{{end}}</small>{{else if eq $role "owner"}} <small>owner</small>{{end}}</li>{{end}}</ul></section>174{{end}}<section><h4>Clone</h4><ul>{{range index $r "clone"}}<li><code>{{.}}</code></li>{{else}}<li>no clone URL advertised</li>{{end}}</ul></section>175{{with index $r "refs"}}<section><h4>Refs</h4><form method="get" action="{{repoPagePath $.Query}}"><label for="ref-select">branch or tag</label><select id="ref-select" name="ref"><option value="">default branch</option>{{range $name, $oid := .}}<option value="{{$name}}"{{if eq ($.Query.Get "ref") $name}} selected{{end}}>{{$name}}</option>{{end}}</select><button>Go</button></form></section>{{end}}{{end}}176177{{define "panel-manage"}}<section><h4>Policy</h4><table><tr><th>name</th><td>{{.Policy.Name}}</td></tr><tr><th>reads</th><td>{{.Policy.Reads}}</td></tr><tr><th>writes</th><td>{{.Policy.Writes}}</td></tr><tr><th>owner</th><td>{{template "nostrKey" .Policy.Owner}}</td></tr></table></section>178{{if .Review}}<section><h4>People</h4><table><tr><th>requests</th><td>{{len (pendingRequests .Requests)}} pending</td></tr></table></section>179{{end}}<section><h4>Session</h4><table><tr><th>you</th><td>{{if .Actor}}{{template "nostrKey" .Actor}}{{else}}guest{{end}}</td></tr><tr><th>role</th><td>{{if .Owner}}owner{{else if .Review}}moderator{{else if .Actor}}member{{else}}none{{end}}</td></tr></table><p><small>Actions need a connected signer.</small></p></section>{{end}}180181{{define "panel-agents"}}{{$counts := agentCounts .Feed}}{{$callbacks := callbackCounts .Callbacks}}<section><h4>Agents</h4><table><tr><th>active</th><td>{{index $counts "active"}}</td></tr><tr><th>paused</th><td>{{index $counts "paused"}}</td></tr><tr><th>revoked</th><td>{{index $counts "revoked"}}</td></tr><tr><th>callbacks</th><td>{{index $callbacks "active"}} active, {{index $callbacks "paused"}} paused</td></tr></table></section>182<section><h4>Machine access</h4><table><tr><th>mcp</th><td><code>/mcp</code> 2026-07-28</td></tr><tr><th>auth</th><td>NIP-98 per request</td></tr><tr><th>rest</th><td><code>/events</code> <code>/query</code></td></tr></table></section>183<section><h4>Kill switch</h4><rpc-form method="pauseallagents" refresh><button data-danger>Pause all agents</button></rpc-form><rpc-form method="resumeallagents" refresh><button>Resume all agents</button></rpc-form><p><small>Paused agents keep their grants.</small></p></section>184<section><h4>Session</h4><table><tr><th>you</th><td>{{if .Actor}}{{template "nostrKey" .Actor}}{{else}}guest{{end}}</td></tr><tr><th>role</th><td>{{if .Owner}}owner{{else if .Actor}}member{{else}}none{{end}}</td></tr></table></section>{{end}}185{{define "panel-rooms"}}{{if eq .Tab "rooms"}}<section><h4>Rooms</h4><table><tr><th>rooms</th><td>{{len .Rooms}}</td></tr><tr><th>protocol</th><td>NIP-29</td></tr></table></section>186<section><h4>Actions</h4><ul>{{if .Actor}}<li><a href="/chat#new-room">new room</a></li>{{else}}<li><a href="{{signinURL .Base .Path .Query}}">sign in</a></li>{{end}}</ul></section>187{{else if .Error}}<section><h4>Room</h4><table><tr><th>id</th><td><code>{{.View}}</code></td></tr></table></section>188{{else}}{{$data := asMap .Event}}{{$room := asMap (index $data "room")}}{{$id := str (index $room "id")}}{{$role := str (index $room "role")}}<section><h4>Room</h4><table><tr><th>id</th><td><code>{{$id}}</code></td></tr><tr><th>access</th><td><status-badge kind="{{if eq (str (index $room "access")) "members"}}members{{else}}open{{end}}">{{if eq (str (index $room "access")) "members"}}members only{{else}}open{{end}}</status-badge></td></tr><tr><th>created</th><td>{{short (index $room "created_at")}}</td></tr>{{if $role}}<tr><th>you</th><td><status-badge>{{$role}}</status-badge></td></tr>{{end}}</table></section>189<section><h4>Members</h4><ul id="members">{{range roomMembers .Event}}{{$m := asMap .}}<li{{if index $m "agent"}} data-agent{{with str (index $m "operator")}} data-operator="{{.}}"{{end}}{{end}}>{{template "nostrName" (str (index $m "pubkey"))}} {{if index $m "agent"}}<status-badge kind="agent">{{if index $m "operator"}}{{template "nostrName" (str (index $m "operator"))}}'s agent{{else}}agent{{end}}</status-badge>{{else}}<small>{{str (index $m "role")}}</small>{{end}}</li>{{else}}<li>{{str (index $room "members")}} members</li>{{end}}</ul></section>190{{if eq .Tab "thread"}}<section><h4>Actions</h4><ul><li><a href="/rooms/{{$id}}">open room</a></li></ul></section>191{{else}}192{{if .Actor}}<section><h4>Actions</h4>{{if roomAdmin $role}}<details><summary>Invite member</summary><room-action room="{{$id}}" kind="9000"><label>pubkey <input name="pubkey" placeholder="npub or 64 hex" required></label><label>role <select name="role"><option value="member">member</option><option value="admin">admin</option>{{if eq $role "owner"}}<option value="owner">owner</option>{{end}}</select></label><button data-primary>Add member</button></room-action></details>193<details><summary>Room settings</summary><room-action room="{{$id}}" kind="9002"><label>name <input name="name" value="{{str (index $room "name")}}" required maxlength="200"></label><label>about <input name="about" value="{{str (index $room "about")}}" maxlength="2000"></label><label>picture <input name="picture" value="{{str (index $room "picture")}}" placeholder="https://"></label><label>access <select name="access"><option value="open"{{if ne (str (index $room "access")) "members"}} selected{{end}}>open</option><option value="members"{{if eq (str (index $room "access")) "members"}} selected{{end}}>members only</option></select></label><button>Save</button></room-action></details>194{{end}}{{if $role}}<room-action room="{{$id}}" kind="9022"><button data-danger>Leave room</button></room-action>{{else}}<room-action room="{{$id}}" kind="9021"><button data-primary>Join room</button></room-action>{{end}}<noscript><p>Room actions need JavaScript and a connected signer.</p></noscript></section>195{{end}}{{end}}{{end}}{{end}}196197{{/* Shared partials. */}}198199{{define "alert"}}{{if .}}<p role="alert">{{friendlyError .}}</p>{{end}}{{end}}200201{{define "nostrKey"}}<nostr-key hex="{{.}}" title="{{.}}">{{shortID .}}</nostr-key>{{end}}202{{/* nostrName shows a person: the vendored nostr-name element swaps the short id for the profile name published on this relay. */}}203{{define "nostrName"}}<nostr-name pubkey="{{.}}" title="{{.}}">{{shortID .}}</nostr-name>{{end}}204205{{define "jsonView"}}<json-view><pre>{{json .}}</pre></json-view>{{end}}206207{{define "nostrEvent"}}{{$e := asMap .}}<nostr-event id="event-{{str (index $e "id")}}">208<header><a href="/e/{{str (index $e "id")}}">{{shortID (index $e "id")}}</a> | kind {{str (index $e "kind")}} | {{template "nostrName" (str (index $e "pubkey"))}}<small><time datetime="{{datetime (index $e "created_at")}}">{{when (index $e "created_at")}}</time></small></header>209{{if eq (str (index $e "kind")) "30023"}}<article>{{markdown (str (index $e "content"))}}</article>{{else}}<pre>{{str (index $e "content")}}</pre>{{end}}210<details><summary>event JSON</summary>{{template "jsonView" .}}</details>211</nostr-event>{{end}}212213{{define "eventList"}}{{if .Feed}}<ol id="events">214{{range .Feed}}<li>{{template "nostrEvent" .}}</li>215{{end}}</ol>216{{else}}<p data-empty>No events.</p>217{{end}}{{end}}