R7 custom emoji in decePub: the server's own, administered, picked, suggested and shown

Administration has a Custom emoji section (EmojiPanel, data-admin-section=emoji): the server's emoji by category, added
with a picture streamed from the file, changed (category, picker, disabled, picture; never the shortcode) and deleted,
asked first, with PrivaPub's 409 and 422 in the dialog. EmojiService reads /clientapi/admin/emoji and, for everyone,
/api/v1/custom_emojis, kept a day in localStorage (CustomEmojiCache) and read again after a change here. EmojiPicker
(a DDropdown sheet on a phone, rendered only while open) offers them by category with common unicode emoji, searchable:
the composer writes the choice at the caret, a card and the announcements banner react with it. The composer suggests
custom emoji after `:` and two letters. Custom reactions toggle as :shortcode: and show their picture, notifications of
one too; located posts, announcements and NavMenu's persona names render the server's emoji.

Verified: Release build, 0 errors (the 3 warnings were there before). Not run against PrivaPub or in the pasture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-18 08:24:43 +02:00
1 parent c572471b88
commit 566b0e33ea
26 files changed
+1385 -56

No files matched your search

+17 -14
View File
@@ -53,6 +53,11 @@
</span>
}
<span>@NotificationText(notification)</span>
@if (notification.Type == "pleroma:emoji_reaction" && notification.EmojiUrl is { Length: > 0 } reactionPicture)
{
<img alt="@($":{notification.Emoji}:")" title="@($":{notification.Emoji}:")" class="h-5 w-5 object-contain" src="@reactionPicture"
data-notification-emoji="@notification.Emoji" />
}
<span class="opacity-60" title="@NerdRow.Time(notification.CreatedAt)">@notification.CreatedAt.GetPassedTime(Localizer)</span>
@if (!Compact)
{
@@ -220,10 +225,10 @@
@foreach (var reaction in status.EmojiReactions)
{
<DButton Size="DSize.Xs" Active="reaction.Me" Disabled="@(IsDefaultDisabled || isActing || Compact || Post.Located)"
OnClick="() => React(reaction.Name, !reaction.Me)" title="@reaction.Name">
OnClick="() => React(ReactionOf(reaction), !reaction.Me)" title="@ReactionOf(reaction)" data-reaction="@reaction.Name">
@if (reaction.Url is { Length: > 0 } image)
{
<img alt="@reaction.Name" class="h-4 w-4 object-contain" src="@image" />
<img alt="@ReactionOf(reaction)" class="h-4 w-4 object-contain" src="@image" />
}
else
{
@@ -333,17 +338,12 @@
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>
</DButton>
<DDropdown @bind-IsOpen="isReactionsOpen" Placement="DDropdownPlacement.Top" CloseOnContentClick="true" SheetOnNarrow="true" class="max-md:w-full"
TriggerClass="@(isReactionsOpen ? "btn btn-sm btn-active max-md:w-full" : "btn btn-sm max-md:w-full")"
ContentClass="mb-2 flex gap-1 p-2 rounded-box max-md:grid max-md:grid-cols-4 max-md:gap-2" title="@Localizer["React"]">
<Trigger><i aria-hidden="true" class="ion-md-happy text-lg"></i></Trigger>
<ChildContent>
@foreach (var emoji in QuickReactions)
{
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => React(emoji, true)">@emoji</DButton>
}
</ChildContent>
</DDropdown>
@* the quick reactions, then the server's custom emoji and the rest; opens downwards, like the menu *@
<EmojiPicker Quick="QuickReactions" OnPick="emoji => React(emoji, true)" ChoicesDisabled="isActing" TriggerLabel="@Localizer["React"]"
class="max-md:w-full" TriggerClass="btn btn-sm max-md:w-full" ActiveTriggerClass="btn btn-sm btn-active max-md:w-full"
data-react>
<i aria-hidden="true" class="ion-md-happy text-lg"></i>
</EmojiPicker>
<span class="hidden md:contents">
<NerdTip Heading="@Localizer["Counts"]" Rows="CountRows(status)" Class="self-center">
<i class="ion-md-stats opacity-40" aria-label="@Localizer["Counts"]"></i>
@@ -1012,7 +1012,9 @@
static readonly string[] QuickReactions = ["👍", "❤️", "😂", "🎉", "🔥", "😮", "😢"];
bool isReactionsOpen;
// what PrivaPub takes for a reaction already on the post: a custom emoji's (it has a picture) as `:shortcode:`, so that
// another server's is found among the post's; a unicode one as it is
static string ReactionOf(EmojiReaction reaction) => reaction.Url is { Length: > 0 } ? $":{reaction.Name}:" : reaction.Name;
async Task React(string emoji, bool add)
{
@@ -1387,6 +1389,7 @@
"poll" => Localizer["A poll you took part in has ended"],
"update" => Localizer["{0} edited a post", name],
"quote" => Localizer["{0} quoted your post", name],
"pleroma:emoji_reaction" when notification.EmojiUrl is { Length: > 0 } => Localizer["{0} reacted with", name],
"pleroma:emoji_reaction" => Localizer["{0} reacted {1}", name, notification.Emoji],
"moderation_warning" => Localizer["This server's staff warned you"],
_ => Localizer["{0}: {1}", name, notification.Type]