{{define "components/status.html"}} {{$s := .Status}} {{$viewer := .Account}} {{$isBoost := $s.BoostOfID}} {{$original := $s.BoostOf}} {{if $isBoost}} {{$s = $original}} {{end}} {{if or (not $s) (not $s.Account)}} {{/* Orphaned boost or a status missing its author (purged/orphaned account): render nothing rather than panic on a nil Account. */}} {{else}}
{{if $isBoost}}
{{T "status.boosted_by"}} {{if $.Status.BoostedBy}} {{$first := index $.Status.BoostedBy 0}} {{if $first.DisplayName}}{{emojifyName $first.DisplayName $first.Emojis}}{{else}}{{$first.Username}}{{end}} {{$others := sub $s.ReblogsCount 1}}{{if and (not (and $viewer $viewer.HideBoostCounts)) (gt $others 0)}}{{Tn $others "notif.and_others" $others}}{{end}} {{else}} {{if $.Status.Account.DisplayName}}{{emojifyName $.Status.Account.DisplayName $.Status.Account.Emojis}}{{else}}{{$.Status.Account.Username}}{{end}} {{end}}
{{end}} {{if and $s.InReplyTo $s.InReplyTo.Account}}
{{T "status.replying_to"}} {{replyMention $s.InReplyTo}}
{{else if $s.InReplyToURI}}
{{T "status.reply_unavailable"}}
{{end}} {{/* The avatar is a direct child of .status, and everything after it is wrapped in .status-main, so a theme can put the avatar in a left gutter with the body beside it (grid column 2) or leave the body at full width beneath it (display:contents) without changing markup. */}} {{/* The author link below goes to the same profile, so exposing this one too would announce every post with a duplicate link. */}}
{{emojifyName $s.Account.DisplayName $s.Account.Emojis}}
@{{$s.Account.Username}} {{if $s.Account.Domain}}@{{$s.Account.Domain}}{{end}}
{{if eq $s.Visibility "public"}}{{else if eq $s.Visibility "direct"}}{{else if eq $s.Visibility "local"}}{{else if eq $s.Visibility "local_unlisted"}}{{else if eq $s.Visibility "unlisted"}}{{else}}{{end}} {{visibilityLabel $s.Visibility}} {{if $s.IsBlogPost}}{{$blogHref := blogPostPath $s}}{{if $blogHref}} {{T "status.blog_post"}}{{end}}{{end}} {{if $s.Language}}{{$s.Language}}{{end}} {{if $s.EditedAt}}{{T "status.edited"}}{{end}} {{if $s.ExpiresAt}} {{T "status.expires_pill" (formatTimePtr $s.ExpiresAt)}}{{end}}
{{if $.Status.FilterMatches}}
{{T "status.filtered"}}{{range $.Status.FilterMatches}}: {{.Filter.Title}}{{end}}
{{end}} {{if $s.Summary}}
{{emojifyName $s.Summary $s.Emojis}} {{template "components/status-media.html" dict "Media" $s.MediaAttachments "Sensitive" $s.Sensitive "DataSaver" (and $viewer $viewer.DataSaver)}} {{template "components/status-body.html" $s}} {{if and (not (and $viewer $viewer.DataSaver)) (not $s.QuoteOf) (previewURL $s.ContentHTML)}}
{{end}}
{{else}} {{template "components/status-media.html" dict "Media" $s.MediaAttachments "Sensitive" $s.Sensitive "DataSaver" (and $viewer $viewer.DataSaver)}} {{template "components/status-body.html" $s}} {{if and (not (and $viewer $viewer.DataSaver)) (not $s.QuoteOf) (previewURL $s.ContentHTML)}}
{{end}} {{end}} {{if $s.QuoteOf}} {{$q := $s.QuoteOf}}
{{if $q.Account.Avatar}}{{end}} {{emojifyName $q.Account.DisplayName $q.Account.Emojis}} @{{$q.Account.Username}}{{if $q.Account.Domain}}@{{$q.Account.Domain}}{{end}}
{{emojifyHTML $q.ContentHTML $q.Emojis}}
{{template "components/status-media.html" dict "Media" $q.MediaAttachments "Sensitive" $q.Sensitive "DataSaver" (and $viewer $viewer.DataSaver)}}
{{end}} {{if $s.Poll}} {{$poll := $s.Poll}} {{$showResults := or (not $poll.HideTotals) $poll.Expired $poll.Voted}}
{{T "status.poll_options"}} {{range $i,$option := $poll.Options}} {{end}} {{end}} {{if $.Status.FilterMatches}}
{{end}} {{if $s.EmojiReactions}}
{{range $reaction := $s.EmojiReactions}} {{if $viewer}}
{{else}} {{if $reaction.Emoji}}{{$reaction.Name}}{{else}}{{$reaction.Name}}{{end}} {{$reaction.Count}} {{end}} {{end}}
{{end}}
{{if $viewer}} {{T "status.action.reply"}} {{if gt $s.RepliesCount 0}}{{$s.RepliesCount}}{{end}} {{if eq $s.Visibility "direct"}} {{else if or (eq $s.Visibility "public") (eq $s.Visibility "unlisted")}}
{{/* Both glyphs ship; CSS shows one per theme. Themes cannot branch in markup; see the note on statusCardKey. Both are aria-hidden, the submit input carries the accessible name. */}} {{if and (not $viewer.HideBoostCounts) (gt $s.ReblogsCount 0)}}{{$s.ReblogsCount}}{{end}}
{{end}} {{if ne $s.Visibility "direct"}} {{T "status.action.quote"}} {{end}}
{{T "status.reaction.choose"}}
{{range $emoji := list "👍" "❤️" "😂" "😮" "😢" "🎉"}} {{$mine := false}}{{range $reaction := $s.EmojiReactions}}{{if and $reaction.Me (eq $reaction.Name $emoji)}}{{$mine = true}}{{end}}{{end}}
{{end}}
{{/* A link, not a button: copying to the clipboard needs JavaScript, and the script cancels this navigation when it runs. Without it the control still leads where it points, instead of being a dead button in every browser that cannot copy. */}} {{T "status.action.copy_link"}} {{if ne $s.AccountID $viewer.ID}} {{T "profile.report"}} {{end}} {{if and $viewer (eq $s.AccountID $viewer.ID)}} {{if $isBoost}} {{else}}
{{T "status.action.edit"}} {{T "status.action.redraft"}} {{end}}
{{end}} {{end}}
{{end}} {{end}} {{/* The post text. A long body is clamped to --status-clamp with a "show more" control under it, rather than pushing the rest of the timeline off a phone screen. The toggle is a label-wrapped checkbox read back by CSS :has(), so it needs no JavaScript (it survives an htmx swap) and no id (the same status can appear twice on one page, as a post and as someone's boost).
would not do: a closed one hides its content outright instead of clamping it. Whether a card is long comes from the content alone, so this markup is safe in the shared card cache; whether a long card actually collapses is left to CSS, which unclamps the focused post of a thread. */}} {{define "components/status-body.html"}} {{if isLongStatus .ContentHTML}}
{{emojifyStatusHTML .ContentHTML .Emojis .QuoteOf}}
{{else}}
{{emojifyStatusHTML .ContentHTML .Emojis .QuoteOf}}
{{end}} {{end}} {{/* Every picture states the size it was measured at, so the browser reserves the right box before the file arrives and the feed stops jumping under the reader's thumb, and paints that box in the picture's own average tone rather than black. A remote attachment nobody measured has neither, and falls back to whatever the stylesheet gives it. */}} {{define "components/status-media.html"}} {{if .Media}}
{{range .Media}} {{$width := mediaWidth .}}{{$height := mediaHeight .}}{{$tone := blurhashColour .Blurhash}}
{{if eq .Type "image"}} {{if $.DataSaver}} {{T "status.load_image"}} {{else}} {{.Description}} {{end}} {{else if eq .Type "video"}} {{else if eq .Type "audio"}} {{else}} {{or .Description .Type}} {{end}} {{if .Description}}
{{.Description}}
{{end}}
{{end}}
{{if .Sensitive}} {{end}}
{{end}} {{end}}