Custom emoji, downvotes, blurhash and focal points on the cards
What other platforms send now shows as they meant it: - custom emoji (a post's or account's :shortcode:) as their pictures in posts, names, content warnings, bios and fields: only the shortcodes the server listed, only http(s) pictures, never inside a tag, the shortcode kept as alt text; - a threadiverse post's downvotes next to its favourites; - blurhash placeholders until a picture loads (wwwroot/js/blurhash.js, a small decoder that watches the page), and a picture's focal point; - an event's online link, participants and capacity, and whether it is tentative or cancelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
5e8fdc43af
commit
2024798732
9 files changed
+236
-9
No files matched your search
@@ -54,7 +54,7 @@
|
||||
{
|
||||
<p class="inline-flex flex-1 items-center gap-2 min-w-0 text-xs">
|
||||
<i class="ion-md-repeat text-info" aria-hidden="true"></i>
|
||||
<a class="font-bold truncate" href="@booster.Url" target="_blank" rel="noopener noreferrer" title="@booster.Acct">@booster.Name</a>
|
||||
<a class="font-bold truncate" href="@booster.Url" target="_blank" rel="noopener noreferrer" title="@booster.Acct">@CustomEmojis.Text(booster.Name, booster.Emojis)</a>
|
||||
<span>@Localizer["boosted"]</span>
|
||||
</p>
|
||||
}
|
||||
@@ -81,7 +81,7 @@
|
||||
<header class="flex flex-wrap items-baseline gap-x-2 gap-y-1 min-w-0 justify-between text-xs md:text-sm">
|
||||
<NerdTip Heading="@Localizer["Author"]" Rows="AuthorRows()" Pending="isReadingServer" OnOpen="ReadServer" Align="DTooltipAlign.Start" Class="min-w-0 flex-1">
|
||||
<span class="inline-flex gap-2 min-w-0 max-w-full">
|
||||
<b class="shrink truncate" title="@status.Account?.Name">@status.Account?.Name</b>
|
||||
<b class="shrink truncate" title="@status.Account?.Name">@CustomEmojis.Text(status.Account?.Name, status.Account?.Emojis ?? [])</b>
|
||||
<span class="flex-1 min-w-6 opacity-50 truncate self-center text-xs">@("@" + status.Account?.Acct)</span>
|
||||
</span>
|
||||
</NerdTip>
|
||||
@@ -137,7 +137,7 @@
|
||||
<DCollapse @bind-IsOpen="isSpoilerOpen" Icon="DCollapseIcon.Arrow" class="rounded-lg neo-xs" HeaderClass="text-sm py-2 min-h-0"
|
||||
ContentClass="text-sm">
|
||||
<Header>
|
||||
<span class="inline-flex gap-2 items-center"><i class="ion-md-warning text-warning"></i>@spoiler</span>
|
||||
<span class="inline-flex gap-2 items-center"><i class="ion-md-warning text-warning"></i>@CustomEmojis.Text(spoiler, status.Emojis)</span>
|
||||
</Header>
|
||||
<ChildContent>
|
||||
@Body(status)
|
||||
@@ -229,6 +229,13 @@
|
||||
<span>@status.FavouritesCount</span>
|
||||
}
|
||||
</DButton>
|
||||
@* a threadiverse post's downvotes: its upvotes are its favourites *@
|
||||
@if (status.Privapub?.Votes is { Down: > 0 } votes)
|
||||
{
|
||||
<span class="inline-flex items-center gap-1 px-1 text-sm opacity-70 tabular-nums" title="@Localizer["Downvotes"]" data-downvotes>
|
||||
<i aria-hidden="true" class="ion-md-arrow-down"></i>@votes.Down
|
||||
</span>
|
||||
}
|
||||
<DButton Size="DSize.Sm" Active="status.Bookmarked" Disabled="IsDefaultDisabled || isActing"
|
||||
OnClick="() => Act(status.Bookmarked ? StatusAction.Unbookmark : StatusAction.Bookmark)" title="@Localizer["Bookmark"]">
|
||||
<i aria-hidden="true" class="ion-md-bookmark text-lg @(status.Bookmarked ? "text-warning" : null)"></i>
|
||||
@@ -400,7 +407,7 @@
|
||||
RenderFragment Body(Status status) => @<div class="flex flex-col gap-3">
|
||||
@if (status.Content is { Length: > 0 } content)
|
||||
{
|
||||
<div class="post-content text-sm md:text-base break-words">@((MarkupString)content)</div>
|
||||
<div class="post-content text-sm md:text-base break-words">@((MarkupString)CustomEmojis.Html(content, status.Emojis))</div>
|
||||
}
|
||||
|
||||
@if (status.MediaAttachments.Count > 0)
|
||||
@@ -492,6 +499,25 @@
|
||||
{
|
||||
<span class="inline-flex gap-2 items-center"><i class="ion-md-pin"></i>@(venue.Name ?? venue.Address ?? venue.Url)</span>
|
||||
}
|
||||
@if (IsWebLink(happening.OnlineUrl))
|
||||
{
|
||||
<a class="inline-flex gap-2 items-center link" href="@happening.OnlineUrl" target="_blank" rel="noopener noreferrer"><i class="ion-md-globe"></i>@Localizer["Online"]</a>
|
||||
}
|
||||
@if (happening.Participants is { } going)
|
||||
{
|
||||
<span class="inline-flex gap-2 items-center tabular-nums">
|
||||
<i class="ion-md-people"></i>@(happening.Capacity is { } capacity ? Localizer["{0} of {1} going", going, capacity] : Localizer["{0} going", going])
|
||||
</span>
|
||||
}
|
||||
@switch (happening.Status?.ToUpperInvariant())
|
||||
{
|
||||
case "CANCELLED":
|
||||
<span class="badge badge-error badge-sm">@Localizer["Cancelled"]</span>
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
<span class="badge badge-warning badge-sm">@Localizer["Tentative"]</span>
|
||||
break;
|
||||
}
|
||||
@if (CanJoin(happening))
|
||||
{
|
||||
<div class="flex flex-wrap gap-2 items-center" data-participation="@(happening.Participation ?? "none")">
|
||||
@@ -544,7 +570,7 @@
|
||||
{
|
||||
"image" => @<a class="block w-full" href="@media.Url" target="_blank" rel="noopener noreferrer">
|
||||
<img alt="@media.Description" title="@media.Description" loading="lazy" class="w-full rounded-lg object-cover @(several ? "aspect-square" : "max-h-[60vh]")"
|
||||
src="@(media.PreviewUrl ?? media.Url)" />
|
||||
src="@(media.PreviewUrl ?? media.Url)" data-blurhash="@media.Blurhash" style="@FocalPoint(media)" />
|
||||
</a>,
|
||||
"gifv" => @<video class="w-full rounded-lg" autoplay muted loop playsinline title="@media.Description" src="@media.Url"></video>,
|
||||
"video" => @<video class="w-full max-h-[50vh] aspect-video rounded-lg neo-inset neo-2xs" controls playsinline preload="metadata"
|
||||
@@ -785,6 +811,13 @@
|
||||
|
||||
static string Number(double? value) => value?.ToString("0.#", System.Globalization.CultureInfo.CurrentCulture) ?? "?";
|
||||
|
||||
// where a cropped picture keeps its subject: Mastodon's focus runs from -1 to 1, left to right and bottom to top
|
||||
static string FocalPoint(MediaAttachment media) => media.Meta?.Focus is { } focus
|
||||
? string.Create(System.Globalization.CultureInfo.InvariantCulture, $"object-position: {(focus.X + 1) * 50:0.#}% {(1 - focus.Y) * 50:0.#}%")
|
||||
: null;
|
||||
|
||||
static bool IsWebLink(string url) => Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Scheme is "https" or "http";
|
||||
|
||||
static string CardHost(string url) => Uri.TryCreate(url, UriKind.Absolute, out var uri) ? uri.Host : url;
|
||||
|
||||
// PrivaPub (like Mastodon) answers a placeholder for a missing header: /media/missing-header.png.
|
||||
|
||||
Reference in new issue
Block a user