| 1 | {{/* Rooms renders the list, one room's timeline with its compose bar and one |
| 2 | thread. room-live receives the room stream after the server-rendered page |
| 3 | establishes the initial state. */}} |
| 4 | {{define "rooms" }}<section id="rooms" > |
| 5 | <h1>Rooms</h1> |
| 6 | {{template "roomList" .}}</section>{{end}} |
| 7 | {{define "roomList" }} |
| 8 | {{if .Error}}{{template "alert" .Error}} |
| 9 | {{else if .Feed}}<table id="rows" > |
| 10 | <thead><tr><th>room</th><th>access</th><th>members</th><th>last message</th></tr></thead> |
| 11 | <tbody> |
| 12 | {{range .Feed}}{{$r := asMap .}}{{$id := str (index $r "id" )}}{{$picture := socialMediaURL (str (index $r "picture" ))}}<tr><td><a href="/rooms/{{$id}}" ><room-avatar picture="{{$picture}}" fallback="{{roomAvatarURL $.URL $id}}" aria-hidden="true" ><img src="{{if $picture}}{{$picture}}{{else}}{{roomAvatarURL $.URL $id}}{{end}}" alt="" width="32" height="32" loading="lazy" ></room-avatar>{{or (str (index $r "name" )) $id}}</a></td><td>{{if eq (str (index $r "access" )) "members" }}members only{{else}}open{{end}}{{with str (index $r "about" )}} <small>{{.}}</small>{{end}}</td><td>{{str (index $r "members" )}}</td><td>{{with index $r "last_message_at" }}<time datetime="{{datetime .}}" >{{when .}}</time>{{else}}no messages{{end}}</td></tr> |
| 13 | {{end}}</tbody> |
| 14 | </table> |
| 15 | {{else}}<p>No rooms yet.</p> |
| 16 | {{end}}{{with (asMap .Event).next_cursor}}<p id="pages" ><a href="{{browsePageURL " /chat" $.Query " cursor" .}}" >next page</a></p> |
| 17 | {{end}}{{if .Actor}}<h2 id="new-room" >New room</h2> |
| 18 | <room-create><label>Name <input name="name" required maxlength="200" placeholder="build" ></label><label>Id <input name="id" maxlength="64" pattern="[a-z0-9_-]{1,64}" placeholder="from the name" ></label><label>Access <select name="access" ><option value="open" >open</option><option value="members" >members only</option></select></label><label>About <textarea name="about" maxlength="2000" ></textarea></label><button>Create room</button></room-create> |
| 19 | <noscript><p>Creating a room needs JavaScript and a connected signer.</p></noscript> |
| 20 | {{else}}<p><a href="{{signinURL .Base .Path .Query}}" >Sign in</a> to create a room.</p> |
| 21 | {{end}} |
| 22 | {{end}} |
| 23 | |
| 24 | {{define "room" }}{{if .Error}}<section id="room" > |
| 25 | <h1>#{{.View}}</h1> |
| 26 | {{template "alert" .Error}} |
| 27 | {{if not .Actor}}<p><a href="{{signinURL .Base .Path .Query}}" >Sign in</a> to read this room.</p>{{end}} |
| 28 | </section> |
| 29 | {{else}}{{$data := asMap .Event}}{{$room := asMap (index $data "room" )}}{{$id := str (index $room "id" )}}{{$picture := socialMediaURL (str (index $room "picture" ))}}<section id="room" data-viewer="{{.Actor}}" > |
| 30 | <h1><room-avatar picture="{{$picture}}" fallback="{{roomAvatarURL $.URL $id}}" aria-hidden="true" ><img src="{{if $picture}}{{$picture}}{{else}}{{roomAvatarURL $.URL $id}}{{end}}" alt="" width="40" height="40" loading="lazy" ></room-avatar>{{or (str (index $room "name" )) $id}} <small>{{if eq (str (index $room "access" )) "members" }}members only{{else}}open room{{end}}</small></h1> |
| 31 | {{with str (index $room "about" )}}<p>{{.}}</p>{{end}} |
| 32 | {{with index $data "next_cursor" }}<p id="pages" ><a href="/rooms/{{$id}}?cursor={{urlquery (str .)}}" >load earlier</a></p>{{end}} |
| 33 | {{if .Query.Get "cursor" }}<p id="newest" ><a href="/rooms/{{$id}}" >newest messages</a></p>{{end}} |
| 34 | {{template "chatActivity" .Activity}} |
| 35 | <div id="messages" >{{range roomItems .Event "messages" .Actor}} |
| 36 | {{template "roomMessage" .}}{{else}}<p id="empty" >No messages yet.</p>{{end}} |
| 37 | </div> |
| 38 | {{if not (.Query.Get "cursor" )}}<room-live room="{{$id}}" ></room-live> |
| 39 | {{template "chatPresence" .}} |
| 40 | {{if .Actor}}<room-compose room="{{$id}}" pubkey="{{.Actor}}" >{{if .Policy.Features.Files}}<room-files aria-label="Attachments" aria-live="polite" ></room-files>{{end}}<room-compose-bar>{{if .Policy.Features.Files}}{{template "roomFilePicker" .}}{{end}}<textarea name="content" rows="1" aria-label="Message" placeholder="Message…" title="Mention someone with @npub or @hex" required></textarea><button type="submit" aria-label="Send" title="Send message" >{{template "roomSendIcon" .}}</button></room-compose-bar></room-compose> |
| 41 | <noscript><p>Sending needs JavaScript and a connected signer.</p></noscript> |
| 42 | {{else}}<p id="guest" ><a href="{{signinURL .Base .Path .Query}}" >Sign in</a> to send messages.</p> |
| 43 | {{end}}{{end}}</section> |
| 44 | {{end}}{{end}} |
| 45 | |
| 46 | {{define "thread" }}{{if .Error}}<section id="thread" > |
| 47 | <h1>Thread</h1> |
| 48 | {{template "alert" .Error}} |
| 49 | </section> |
| 50 | {{else}}{{$data := asMap .Event}}{{$room := asMap (index $data "room" )}}{{$id := str (index $room "id" )}}{{$picture := socialMediaURL (str (index $room "picture" ))}}{{$root := roomRoot .Event .Actor}}<section id="thread" data-viewer="{{.Actor}}" > |
| 51 | <p id="crumbs" ><a href="/rooms/{{$id}}" ><room-avatar picture="{{$picture}}" fallback="{{roomAvatarURL $.URL $id}}" aria-hidden="true" ><img src="{{if $picture}}{{$picture}}{{else}}{{roomAvatarURL $.URL $id}}{{end}}" alt="" width="28" height="28" loading="lazy" ></room-avatar>{{or (str (index $room "name" )) $id}}</a></p> |
| 52 | <h1>Thread <small>{{$root.Replies}} {{if eq $root.Replies 1}}reply{{else}}replies{{end}}</small></h1> |
| 53 | <div id="root" >{{template "roomMessage" $root}}</div> |
| 54 | {{template "chatActivity" .Activity}} |
| 55 | <div id="messages" >{{range roomItems .Event "replies" .Actor}} |
| 56 | {{template "roomMessage" .}}{{else}}<p id="empty" >No replies yet.</p>{{end}} |
| 57 | </div> |
| 58 | {{with index $data "next_cursor" }}<p id="pages" ><a href="/rooms/{{$id}}/thread/{{$root.ID}}?cursor={{urlquery (str .)}}" >load earlier</a></p>{{end}} |
| 59 | {{if not (.Query.Get "cursor" )}}<room-live room="{{$id}}" root="{{$root.ID}}" ></room-live> |
| 60 | {{template "chatPresence" .}} |
| 61 | {{if .Actor}}<room-compose room="{{$id}}" pubkey="{{.Actor}}" kind="{{if eq $root.Kind " 11"}}12{{else}}9{{end}}" root="{{$root.ID}}" root-pubkey="{{$root.PubKey}}" >{{if .Policy.Features.Files}}<room-files aria-label="Attachments" aria-live="polite" ></room-files>{{end}}<room-compose-bar>{{if .Policy.Features.Files}}{{template "roomFilePicker" .}}{{end}}<textarea name="content" rows="1" aria-label="Reply" placeholder="Reply in this thread…" required></textarea><button type="submit" aria-label="Reply" title="Send reply" >{{template "roomSendIcon" .}}</button></room-compose-bar></room-compose> |
| 62 | <noscript><p>Replying needs JavaScript and a connected signer.</p></noscript> |
| 63 | {{else}}<p id="guest" ><a href="{{signinURL .Base .Path .Query}}" >Sign in</a> to reply.</p> |
| 64 | {{end}}{{end}}</section> |
| 65 | {{end}}{{end}} |
| 66 | |
| 67 | {{define "roomFilePicker" }}<button type="button" data-attach aria-label="Attach files" title="Attach files, or paste or drop them. Up to 8 files, 32 MiB each." ><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" ><path d="m21 11-8.5 8.5a6 6 0 0 1-8.5-8.5l9-9a4 4 0 0 1 5.7 5.7l-9 9a2 2 0 0 1-2.8-2.8l8.5-8.5" /></svg></button><input type="file" name="attachments" aria-label="Attach files" multiple hidden>{{end}} |
| 68 | {{define "roomSendIcon" }}<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" ><path d="M12 19V5m-7 7 7-7 7 7" /></svg>{{end}} |
| 69 | |
| 70 | {{/* One message. room-live builds the same markup for messages that arrive on the stream. */}} |
| 71 | {{define "roomMessage" }}<room-message id="msg-{{.ID}}" data-id="{{.ID}}" data-kind="{{.Kind}}" {{if .Root}} data-root="{{.Root}}" {{end}} data-updated-at="{{.UpdatedAt}}" data-pubkey="{{.PubKey}}" {{if .Own}} data-own{{end}}{{if .Agent}} data-agent{{end}}{{if .Notice}} data-notice{{end}}{{if .Edited}} data-edited{{end}}><nostr-avatar pubkey="{{.PubKey}}" aria-hidden="true" ><img src="{{avatarURL .PubKey}}" alt="" width="32" height="32" loading="lazy" ></nostr-avatar> |
| 72 | <header><b>{{template "nostrName" .PubKey}}</b>{{if .Own}}<span data-own-label>you</span>{{end}}{{if .Agent}} | {{if .Operator}}{{template "nostrName" .Operator}}'s agent{{else}}agent{{end}}{{else if .Role}} | {{.Role}}{{end}}<small><time datetime="{{datetime .CreatedAt}}" title="{{when .CreatedAt}}">{{clock .CreatedAt}}</time></small></header> |
| 73 | {{if .Notice}}<p>{{.Notice}}</p>{{else}}{{if .Quote}}<blockquote data-quote><a href="/open?target={{urlquery (printf " nostr:%s" .Quote)}}" >quoted event {{shortID .Quote}}</a></blockquote>{{end}}<div>{{chatMarkdown (roomAttachmentContent .Content .Attachments)}}</div>{{roomAttachmentMarkup .Attachments}}{{end}} |
| 74 | {{if or .Mentions (and (not .InThread) (or (eq .Kind "11" ) (eq .Kind "9" ) (eq .Kind "40002" ) .Root)) .Reactions .Edited}}<footer>{{if .Mentions}}<span>to {{range .Mentions}}{{template "nostrName" .}} {{end}}</span>{{end}}{{if not .InThread}}{{if .Root}}<a href="/rooms/{{.Room}}/thread/{{.Root}}" >in thread</a>{{else if or (eq .Kind "11" ) (eq .Kind "9" ) (eq .Kind "40002" )}}<a href="/rooms/{{.Room}}/thread/{{.ID}}" >thread{{if .Replies}} | {{.Replies}} {{if eq .Replies 1}}reply{{else}}replies{{end}}{{end}}</a>{{end}}{{end}}{{range .Reactions}}<span data-reaction="{{.Content}}" >{{.Content}} {{.Count}}</span>{{end}}{{if .Edited}}<span data-edited>edited</span>{{end}}</footer>{{end}} |
| 75 | </room-message>{{end}} |
| 76 | |
| 77 | {{define "chatPresence" }}<chat-presence room="{{.View}}" actor="{{.Actor}}" ><output aria-live="polite" ></output></chat-presence>{{end}} |