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:
1 parent
c572471b88
commit
566b0e33ea
26 files changed
+1385
-56
No files matched your search
@@ -2,7 +2,8 @@
|
||||
|
||||
@* The administrators' announcements shown now (/api/v1/announcements) that the persona has not dismissed, one at a time
|
||||
above the feed's tabs: their words (sanitized HTML from the administrator's Markdown), when they run, and the
|
||||
reactions personas left, which the persona adds and takes back with a unicode emoji. Dismissed, one is gone for this
|
||||
reactions personas left, which the persona adds and takes back with a unicode emoji or one of the server's custom ones
|
||||
(EmojiPicker; its text shows the custom emoji it names). Dismissed, one is gone for this
|
||||
persona. New, changed and removed ones, and the reactions' counts, come through the user stream
|
||||
(StreamingService.HearAnnouncements); the banner renders itself and never touches the feed's cards. *@
|
||||
@if (Current is { } current)
|
||||
@@ -39,15 +40,15 @@
|
||||
{
|
||||
<p class="text-xs opacity-70" data-announcement-when>@When(current)</p>
|
||||
}
|
||||
<div class="post-content doc-content text-sm break-words max-h-28 md:max-h-40 overflow-y-auto">@((MarkupString)current.Content)</div>
|
||||
<div class="post-content doc-content text-sm break-words max-h-28 md:max-h-40 overflow-y-auto">@((MarkupString)CustomEmojis.Html(current.Content, current.Emojis))</div>
|
||||
<div class="flex flex-wrap items-center gap-1 pointer-coarse:gap-2" data-announcement-reactions>
|
||||
@foreach (var reaction in current.Reactions)
|
||||
{
|
||||
<DButton Size="DSize.Xs" Active="reaction.Me == true" Disabled="IsDefaultDisabled || isActing" OnClick="() => React(reaction.Name, reaction.Me != true)"
|
||||
title="@reaction.Name" aria-pressed="@(reaction.Me == true ? "true" : "false")" data-announcement-reaction="@reaction.Name">
|
||||
title="@NameOf(reaction)" aria-pressed="@(reaction.Me == true ? "true" : "false")" data-announcement-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="@NameOf(reaction)" class="h-4 w-4 object-contain" src="@image" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -56,17 +57,10 @@
|
||||
<span class="tabular-nums">@reaction.Count</span>
|
||||
</DButton>
|
||||
}
|
||||
<DDropdown @bind-IsOpen="isReactionsOpen" Placement="DDropdownPlacement.Bottom" CloseOnContentClick="true" SheetOnNarrow="true"
|
||||
TriggerClass="@(isReactionsOpen ? "btn btn-xs btn-active" : "btn btn-xs")"
|
||||
ContentClass="mt-2 flex gap-1 p-2 rounded-box max-md:grid max-md:grid-cols-4 max-md:gap-2" title="@Localizer["React"]" data-announcement-react>
|
||||
<Trigger><i aria-hidden="true" class="ion-md-happy"></i></Trigger>
|
||||
<ChildContent>
|
||||
@foreach (var emoji in QuickReactions)
|
||||
{
|
||||
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => React(emoji, true)" data-announcement-react-emoji="@emoji">@emoji</DButton>
|
||||
}
|
||||
</ChildContent>
|
||||
</DDropdown>
|
||||
<EmojiPicker Quick="QuickReactions" OnPick="emoji => React(emoji, true)" ChoicesDisabled="isActing" TriggerLabel="@Localizer["React"]"
|
||||
TriggerClass="btn btn-xs" ActiveTriggerClass="btn btn-xs btn-active" data-announcement-react>
|
||||
<i aria-hidden="true" class="ion-md-happy"></i>
|
||||
</EmojiPicker>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
@@ -76,13 +70,16 @@
|
||||
|
||||
[Inject] AnnouncementService AnnouncementService { get; set; }
|
||||
[Inject] StreamingService Streaming { get; set; }
|
||||
[Inject] EmojiService EmojiService { get; set; }
|
||||
|
||||
List<Announcement> announcements = [];
|
||||
int index;
|
||||
bool isActing;
|
||||
bool isReactionsOpen;
|
||||
string loadedPersonaId;
|
||||
|
||||
// a custom emoji's reaction is named by its shortcode
|
||||
static string NameOf(AnnouncementReaction reaction) => reaction.Url is { Length: > 0 } ? $":{reaction.Name}:" : reaction.Name;
|
||||
|
||||
// what the persona has not dismissed, oldest first as the server lists them
|
||||
List<Announcement> Shown => [.. announcements.Where(a => a.Read != true)];
|
||||
|
||||
@@ -188,8 +185,10 @@
|
||||
reacted.Reactions = reaction.Count <= 0
|
||||
? [.. reacted.Reactions.Where(r => r.Name != reaction.Name)]
|
||||
: known is null
|
||||
? [.. reacted.Reactions, new AnnouncementReaction { Name = reaction.Name, Count = reaction.Count, Me = false }]
|
||||
: [.. reacted.Reactions.Select(r => r.Name == reaction.Name ? new AnnouncementReaction { Name = r.Name, Count = reaction.Count, Me = r.Me, Url = r.Url } : r)];
|
||||
? [.. reacted.Reactions, NewReaction(reaction)]
|
||||
: [.. reacted.Reactions.Select(r => r.Name == reaction.Name
|
||||
? new AnnouncementReaction { Name = r.Name, Count = reaction.Count, Me = r.Me, Url = r.Url, StaticUrl = r.StaticUrl }
|
||||
: r)];
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
@@ -198,6 +197,12 @@
|
||||
StateHasChanged();
|
||||
});
|
||||
|
||||
// one the stream tells of first: the event names it only, so a custom emoji's pictures come from the server's emoji read
|
||||
// already (none while they aren't, until the list is read again)
|
||||
AnnouncementReaction NewReaction(AnnouncementReactionEvent reaction) => EmojiService.Known(reaction.Name) is { } custom
|
||||
? new AnnouncementReaction { Name = reaction.Name, Count = reaction.Count, Me = false, Url = custom.Url, StaticUrl = custom.StaticUrl }
|
||||
: new AnnouncementReaction { Name = reaction.Name, Count = reaction.Count, Me = false };
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
Streaming.Announced -= OnAnnounced;
|
||||
|
||||
Reference in new issue
Block a user