clone

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

feat/tinyagent
tinyrelay / tinyclient / social.html
social.html 15.3K
1{{define "social"}}{{$view := socialView .Query}}<section id="social">2<header id="social-head"><h1>{{$view.Label}}</h1><p>{{$view.Summary}}</p>{{if eq $view.Key "podcasts"}}<p id="podcast-subscribe"><a href="{{podcastFeedURL .Query}}">Subscribe with RSS</a></p>{{end}}</header>3{{if .Error}}{{template "alert" .Error}}{{else}}4{{if eq $view.Key "podcasts"}}{{template "podcastShow" .}}{{end}}5{{if .Actor}}{{if eq $view.Key "podcasts"}}{{template "podcastComposer" .}}{{else}}{{template "socialComposer" .}}{{end}}{{else}}<p data-social-welcome><a href="{{signinURL .Base .Path .Query}}">Sign in</a> to join the conversation.</p>{{end}}6{{with .Query.Get "author"}}<p id="social-author-filter">Posts by {{template "nostrName" .}} <a href="/social/profile?author={{.}}">View profile →</a></p>{{end}}7{{template "socialFeed" .}}8{{end}}<noscript><p>Reading works without JavaScript. Publishing needs JavaScript and a connected signer.</p></noscript>9</section>{{end}}1011{{define "socialFeed"}}12{{if .Feed}}<ol id="social-feed">{{range .Feed}}{{template "socialPost" (socialContext . $)}}{{end}}</ol>13{{else}}<div id="social-empty"><h2>{{if .Query.Get "q"}}No matching posts{{else if eq .Tab "social-profile"}}No posts yet{{else if eq (socialView .Query).Key "feed"}}A place to share what’s on your mind{{else}}Nothing here yet{{end}}</h2><p>{{if .Query.Get "q"}}Try another search or clear the filters.{{else if eq .Tab "social-profile"}}Posts from this person will appear here.{{else if eq (socialView .Query).Key "feed"}}Share a note, a story or something you made.{{else}}Posts shared in this category will appear here.{{end}}</p></div>{{end}}14{{with index (asMap .Event) "next_cursor"}}<p id="social-pages"><a href="{{socialFeedPage $ (str .)}}">Older posts →</a></p>{{end}}15{{end}}1617{{define "social-thread"}}{{$post := socialContext (index (asMap .Event) "post") .}}<section id="social-thread">18<p data-social-back><a href="/social">← Social</a></p>19{{if .Error}}{{template "alert" .Error}}{{else}}20{{if index $post "parent_url"}}<p data-thread-context><a href="{{index $post "parent_url"}}">↑ Parent post</a>{{if ne (index $post "parent_url") (index $post "root_url")}}{{with index $post "root_url"}} <a href="{{.}}">View full conversation</a>{{end}}{{end}}</p>{{end}}21<ol id="social-thread-post">{{template "socialPost" $post}}</ol>22<div id="social-comments"><h2>Conversation{{with index $post "reply_count"}} <small>{{.}}</small>{{end}}</h2>23{{if .Actor}}{{template "socialCommentComposer" $post}}{{else}}<p><a href="{{signinURL .Base .Path .Query}}">Sign in</a> to reply.</p>{{end}}24{{with index (asMap .Event) "comments"}}<ol>{{range .}}{{template "socialComment" (socialContext . $)}}{{end}}</ol>{{else}}<p data-social-empty-replies>No replies yet. Start the conversation.</p>{{end}}25{{with index (asMap .Event) "next_cursor"}}<p><a href="{{socialThreadPage $.Path $.Query (str .)}}">Older replies →</a></p>{{end}}26</div>{{end}}</section>{{end}}2728{{define "socialPost"}}{{$p := asMap .}}<li data-social-post data-kind="{{str (index $p "kind")}}">29{{template "socialByline" $p}}30{{if eq (str (index $p "kind")) "30023"}}31<article data-post-body="article">32{{if index $p "detail"}}<h1>{{or (str (index $p "title")) "Untitled article"}}</h1>{{else}}<h2><a href="{{socialURL $p}}">{{or (str (index $p "title")) "Untitled article"}}</a></h2>{{end}}33{{with index $p "summary"}}<p data-post-summary>{{str .}}</p>{{else}}{{if not (index $p "detail")}}<p data-post-summary>{{socialPreview $p}}</p>{{end}}{{end}}34{{with socialMediaURL (str (index $p "image"))}}<a href="{{socialURL $p}}" data-post-media><img src="{{.}}" alt="{{or (str (index $p "title")) "Article cover"}}" loading="lazy" referrerpolicy="no-referrer"></a>{{end}}35{{if index $p "detail"}}<div data-article-content>{{socialBody $p}}</div>{{end}}36{{with index $p "read_minutes"}}<small data-read-time>{{.}} min read{{if not (index $p "detail")}} <a href="{{socialURL $p}}">Read article →</a>{{end}}</small>{{end}}37</article>38{{else if eq (str (index $p "kind")) "54"}}{{$episode := podcastEpisode $p}}<article data-post-body="podcast">39{{if index $p "detail"}}<h1>{{or $episode.Title "Untitled episode"}}</h1>{{else}}<h2><a href="{{socialURL $p}}">{{or $episode.Title "Untitled episode"}}</a></h2>{{end}}40{{with $episode.Image}}<img data-podcast-cover src="{{.}}" alt="" loading="lazy" referrerpolicy="no-referrer">{{end}}41{{with $episode.Description}}<p>{{.}}</p>{{end}}42{{range $episode.Audio}}<audio controls preload="none" src="{{.URL}}"><a href="{{.URL}}">Download audio</a></audio>{{end}}43{{if index $p "detail"}}<div data-article-content>{{podcastNotes $p}}</div>{{end}}44<p data-podcast-subscribe><a href="/social/podcasts.rss?author={{index $p "pubkey"}}">Subscribe to this podcast</a></p>45</article>46{{else}}<article data-post-body="note">{{socialBody $p}}</article>{{end}}47{{template "socialActions" $p}}48</li>{{end}}4950{{define "socialByline"}}{{$p := asMap .}}<header data-social-byline><span data-social-avatar><nostr-avatar pubkey="{{index $p "pubkey"}}" aria-hidden="true">{{with socialMediaURL (str (index $p "author_picture"))}}<img src="{{.}}" alt="" loading="lazy" width="36" height="36" referrerpolicy="no-referrer">{{else}}<img src="{{avatarURL (str (index $p "pubkey"))}}" alt="" width="36" height="36" loading="lazy">{{end}}</nostr-avatar></span><span><a href="/social/profile?author={{str (index $p "pubkey")}}{{if eq (str (index $p "kind")) "54"}}&amp;kind=podcasts{{end}}"><b>{{if index $p "author_name"}}{{str (index $p "author_name")}}{{else}}{{template "nostrName" (str (index $p "pubkey"))}}{{end}}</b></a> <a href="{{socialURL $p}}"><time datetime="{{datetime (index $p "created_at")}}">{{when (index $p "created_at")}}</time></a>{{if eq (str (index $p "kind")) "30023"}} <small>Article</small>{{end}}{{with index $p "parent_url"}}<small data-social-parent>↳ <a href="{{.}}">Replying{{if index $p "parent_author"}} to {{template "nostrName" (str (index $p "parent_author"))}}{{else}} to a post{{end}}</a></small>{{end}}</span></header>{{end}}5152{{define "socialActions"}}{{$p := asMap .}}<footer data-social-actions>53<a href="{{socialURL $p}}#reply">{{if index $p "reply_count"}}{{index $p "reply_count"}} {{if eq (str (index $p "reply_count")) "1"}}reply{{else}}replies{{end}}{{else}}Reply{{end}}</a>54{{if index $p "viewer"}}<social-reaction actor="{{index $p "viewer"}}" event="{{index $p "id"}}" pubkey="{{index $p "pubkey"}}" kind="{{index $p "kind"}}" address="{{index $p "address"}}" relay="{{index $p "relay"}}" content="+"><button type="button" aria-label="Like this post" aria-pressed="{{socialReacted $p "+"}}"{{if socialReacted $p "+"}} disabled{{end}}>♥ {{socialReactionCount $p "+"}}</button></social-reaction>{{else}}<span aria-label="Likes">♥ {{socialReactionCount $p "+"}}</span>{{end}}55{{range index $p "reactions"}}{{$r := asMap .}}{{if ne (str (index $r "content")) "+"}}<span title="Reaction">{{with socialMediaURL (str (index $r "image"))}}<img data-reaction-emoji src="{{.}}" alt="{{index $r "content"}}" width="20" height="20" loading="lazy" referrerpolicy="no-referrer">{{else}}{{str (index $r "content")}}{{end}} {{index $r "count"}}</span>{{end}}{{end}}56{{if index $p "viewer"}}<details data-social-react><summary>React</summary>{{range strs "👍" "❤️" "😂" "🎉" "🤔"}}<social-reaction actor="{{index $p "viewer"}}" event="{{index $p "id"}}" pubkey="{{index $p "pubkey"}}" kind="{{index $p "kind"}}" address="{{index $p "address"}}" relay="{{index $p "relay"}}" content="{{.}}"><button type="button" aria-label="React {{.}}" aria-pressed="{{socialReacted $p .}}"{{if socialReacted $p .}} disabled{{end}}>{{.}}</button></social-reaction>{{end}}</details>{{end}}57{{if index $p "detail"}}<share-link url="{{socialURL $p}}"></share-link>{{else}}<a href="{{socialURL $p}}">Open</a>{{end}}58</footer>{{end}}5960{{define "socialComposer"}}{{$article := eq (.Query.Get "compose") "article"}}61<nav id="social-write" aria-label="Write"><a href="{{socialQuery .Query "compose" ""}}"{{if not $article}} aria-current="page"{{end}}>Write a note</a><a href="{{socialQuery .Query "compose" "article"}}"{{if $article}} aria-current="page"{{end}}>Write an article</a></nav>62<social-compose mode="{{if $article}}article{{else}}note{{end}}" actor="{{.Actor}}" relay="{{wsURL .URL}}" refresh="/social">63{{if $article}}<label>Title <input name="title" required maxlength="200" placeholder="Give your story a title"></label><label>Subtitle <input name="summary" maxlength="500" placeholder="A short introduction"></label>{{end}}64<label>{{if $article}}Article{{else}}Note{{end}} <textarea name="content" rows="{{if $article}}14{{else}}3{{end}}" maxlength="65000" placeholder="{{if $article}}Tell your story. Markdown is supported.{{else}}What’s on your mind?{{end}}"></textarea></label>65{{if $article}}<label>Cover image URL <input name="cover" type="url" placeholder="https://"></label>{{end}}66{{template "socialMediaFields" .}}67<details data-post-options><summary>Topics</summary><label>Tags <input name="tags" placeholder="nature, design, notes"></label></details>68<p data-compose-actions><button type="submit">{{if $article}}Publish article{{else}}Post note{{end}}</button> <button type="button" data-preview>Preview</button> <small>Draft saved in this browser.</small></p><div data-preview-output hidden></div>69</social-compose>{{end}}7071{{define "socialMediaFields"}}<details data-post-options><summary>Attach media</summary><label>Images, video or audio <input type="file" name="media-file" accept="image/*,video/*,audio/*" multiple></label><label>Or paste a media URL <input name="media" type="url" placeholder="https://"></label><label>Description <input name="alt" maxlength="1000" placeholder="Describe the media for readers"></label><small>Attachments are published with your post.</small></details>{{end}}7273{{define "podcastShow"}}{{with index (asMap .Event) "show"}}{{$show := asMap .}}{{if index $show "id"}}<header id="podcast-show">74{{with socialMediaURL (str (index $show "image"))}}<img src="{{.}}" alt="" width="96" height="96" loading="lazy" referrerpolicy="no-referrer">{{end}}75<h2>{{or (str (index $show "title")) "Untitled podcast"}}</h2><p>{{str (index $show "description")}}</p>76{{range index $show "websites"}}<p><a href="{{socialMediaURL (str .)}}" rel="noopener noreferrer">{{.}}</a></p>{{end}}77{{with index $show "authors"}}<p>{{range .}}{{$author := asMap .}}<a href="/social/profile?author={{index $author "pubkey"}}">{{template "nostrName" (str (index $author "pubkey"))}}</a>{{with index $author "role"}} <small>{{.}}</small>{{end}} {{end}}</p>{{end}}78</header>{{end}}{{end}}{{end}}7980{{define "podcastComposer"}}81<p id="podcast-own"><a href="/social?kind=podcasts&amp;author={{.Actor}}">Your podcast</a></p>82{{if or (not (.Query.Get "author")) (eq (.Query.Get "author") .Actor)}}83<details id="podcast-publish"><summary>Publish an episode</summary>84<p>Episodes publish under your connected podcast identity.</p>85<social-compose mode="podcast" actor="{{.Actor}}" relay="{{wsURL .URL}}" refresh="/social?kind=podcasts&amp;author={{.Actor}}">86<label>Episode title <input name="title" required maxlength="200"></label>87<label>Description <input name="summary" maxlength="1000"></label>88<label>Audio file <input type="file" name="media-file" accept="audio/*"></label>89<label>Or use an audio URL <input name="media" type="url" placeholder="https://"></label>90<label>Audio format <select name="media-type"><option value="">Detect from file or URL</option><option value="audio/mpeg">MP3</option><option value="audio/mp4">M4A</option><option value="audio/ogg">Ogg</option><option value="audio/wav">WAV</option><option value="audio/flac">FLAC</option></select></label>91<label>Episode artwork URL <input name="cover" type="url" placeholder="https://"></label>92<label>Show notes <textarea name="content" rows="6" maxlength="65000" placeholder="Links and notes for this episode. Markdown is supported."></textarea></label>93<p data-compose-actions><button type="submit">Publish episode</button> <button type="button" data-preview>Preview notes</button></p><div data-preview-output hidden></div>94</social-compose></details>95{{if eq (.Query.Get "author") .Actor}}{{$show := asMap (index (asMap .Event) "show")}}96<details id="podcast-settings"><summary>Podcast details</summary>97<social-compose mode="podcast-show" actor="{{.Actor}}" relay="{{wsURL .URL}}" refresh="/social?kind=podcasts&amp;author={{.Actor}}" show-tags="{{json (index $show "tags")}}">98<label>Podcast title <input name="title" value="{{index $show "title"}}" required maxlength="200"></label>99<label>Description <textarea name="summary" rows="3" maxlength="4000">{{index $show "description"}}</textarea></label>100<label>Artwork URL <input name="cover" type="url" value="{{index $show "image"}}" placeholder="https://"></label>101<label>Website <input name="website" type="url" value="{{with index $show "websites"}}{{index . 0}}{{end}}" placeholder="https://"></label>102<button type="submit">Save podcast details</button>103</social-compose></details>{{end}}104{{end}}{{end}}105106{{define "socialCommentComposer"}}{{$p := asMap .}}<social-compose id="reply" mode="comment" actor="{{index $p "viewer"}}" relay="{{index $p "relay"}}" target="{{index $p "id"}}" target-kind="{{index $p "kind"}}" target-pubkey="{{index $p "pubkey"}}" target-tags="{{json (index $p "tags")}}" address="{{or (str (index $p "root_address")) (str (index $p "address"))}}" root="{{or (str (index $p "root_id")) (str (index $p "id"))}}" root-pubkey="{{str (index $p "root_author")}}" root-kind="{{or (str (index $p "root_kind")) (str (index $p "kind"))}}" refresh="{{socialURL $p}}">107<label>Reply <textarea name="content" rows="3" maxlength="20000" placeholder="Add to the conversation"></textarea></label>{{template "socialMediaFields" .}}<p data-compose-actions><button type="submit">Post reply</button> <button type="button" data-preview>Preview</button></p><div data-preview-output hidden></div>108</social-compose>{{end}}109110{{define "socialComment"}}{{$c := asMap .}}<li data-social-comment{{if index $c "nested"}} data-nested-reply{{end}}>{{template "socialByline" $c}}<div data-comment-body>{{socialBody $c}}</div>{{template "socialActions" $c}}</li>{{end}}111112{{define "panel-social"}}113{{if eq .Tab "social-thread"}}<section><h4>Conversation</h4><p>Follow the discussion or add a reply of your own.</p><p><a href="/social">Back to feed</a></p></section>114{{else if eq .Tab "social-profile"}}<section><h4>Profile</h4>{{if .View}}<p>This person’s posts, newest first.</p><p><a href="{{podcastFeedURL .Query}}">Subscribe to podcasts</a></p>{{else}}<p>Open your profile or look up another person.</p>{{end}}{{if .Actor}}<p><a href="/social/profile">Your profile</a></p>{{end}}<p><a href="/social">Browse the feed</a></p></section>115{{else}}{{$view := socialView .Query}}<section><h4>{{$view.Label}}</h4><p>{{$view.Summary}}</p><p>Newest first. Replies and reactions stay with their posts.</p></section>116<section><h4>Find a post</h4><form action="/social" method="get"><input type="hidden" name="kind" value="{{$view.Key}}"><label>Search <input name="q" type="search" value="{{.Query.Get "q"}}" placeholder="Words or a title"></label><label>Author <input name="author" value="{{.Query.Get "author"}}" placeholder="Public key"></label><button>Search</button> <a href="{{$view.URL}}">Clear</a></form></section>{{end}}117<section><h4>Follow the feed</h4><p><a href="/social.xml">Atom</a> | <a href="/social.json">JSON Feed</a></p></section>118{{if not .Actor}}<section><h4>Join in</h4><p><a href="{{signinURL .Base .Path .Query}}">Sign in</a> to post, reply and react.</p></section>{{end}}{{end}}