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

+51 -6
View File
@@ -319,7 +319,8 @@ Never redeclare the base's members in a component.
ban, two-factor reset and deletion, and a persona's or a group's deletion with the administrator's password on
`/clientapi/admin`), `AboutService` (what the server says of itself to anyone: the instance API, its documents
and published domain blocks, never with credentials), `AnnouncementService` (`/api/v1/announcements`: dismissing and
reacting), `MediaService` (uploads, and deleting those no post holds), `GroupService`, `InstanceService` and
reacting), `EmojiService` (the server's custom emoji: `/api/v1/custom_emojis` for everyone, cached, and
`/clientapi/admin/emoji` for administrators; see **Custom emoji**), `MediaService` (uploads, and deleting those no post holds), `GroupService`, `InstanceService` and
`ProvenanceService`.
- **Reactions and votes are public** (owner decision, PrivaPub's ROADMAP): before a persona's first reaction, poll vote
or downvote, `PublicNoticeService` asks it once (`PublicNoticeHost` in `MainLayout`) and remembers the persona in
@@ -328,7 +329,8 @@ Never redeclare the base's members in a component.
shows who takes part. An event card shows the persona's participation (`privapub.event.participation`: pending,
accepted, rejected) with Join or Leave, except for events by invitation or taken on another site (`join_mode`).
- **What a card shows of other platforms:** custom emoji (`Helpers/CustomEmojis.cs`: a post's or account's `:shortcode:`
in its text, name, warning, bio and fields, as the picture the server listed, never inside a tag), a threadiverse
in its text, name, warning, bio and fields, as the picture the server listed, never inside a tag; this server's own
come in `emojis` the same way, see **Custom emoji**), a threadiverse
post's downvotes next to its favourites with the persona's own as a toggle (`data-downvote`, in More on a phone as
`data-downvote-phone`; `StatusAction.Downvote`/`Undownvote`, PrivaPub's `/statuses/:id/downvote|undownvote`, answered
with the post, its `privapub.votes.downvoted` set and its favourite dropped). PrivaPub sends votes on every post, so
@@ -407,7 +409,8 @@ Never redeclare the base's members in a component.
(`media_attributes`). Its visibility and poll stay.
- A new post's media are sensitive when the persona's "always mark media as sensitive" (`source.sensitive`) says so.
A mention or hashtag being written at the end of the words is offered what PrivaPub already knows
(`/api/v2/search`, never resolving), once typing pauses.
(`/api/v2/search`, never resolving), once typing pauses; a custom emoji (`:` and two letters at least) is offered the
server's own at once, and the emoji button opens the picker (see **Custom emoji**).
An edited post's card says so, and its versions open from there (`/statuses/:id/history`, newest first; PrivaPub
keeps a remote post's earlier versions from its edits and from Pleroma's `formerRepresentations`).
- **Threads** (`Pages/Thread.razor` at `/expand/{id}`): opening a remote thread makes PrivaPub read its replies
@@ -455,7 +458,7 @@ Never redeclare the base's members in a component.
Blocked servers (`blocks`), Relays (`relays`), Media limits (`media`), Statistics (`statistics`) and Backups
(`backups`). Moderators also see Accounts (`accounts`), administrators Users & personas (`users`). PrivaPub checks the
role on every call (moderators: `/clientapi/moderator`, administrators: `/clientapi/admin`, checked against its database
too). Custom emoji come with R7: until then they have no section.
too). Administrators also see Custom emoji (`emoji`, after Announcements; see **Custom emoji**).
- **Users & personas** (`UsersPanel`, `data-users`, `/clientapi/admin/roots`; the one place a login is shown with its
personas): newest first, 50 at a time (`data-users-more`), found by a login's or a persona's name (`data-users-search`;
`data-users-reload`). Each login (`data-user="{login name}"`, `data-user-role` user|moderator|admin,
@@ -526,6 +529,48 @@ Never redeclare the base's members in a component.
- **A post from the feed:** a moderator removes another's post from its card's menu (`data-moderate-remove-post`,
`ActDialog`, `/clientapi/moderator/posts/remove`): one written here is deleted as its author would, and its author
warned; a copy of one from elsewhere is hidden here. The page takes the card out.
- **Custom emoji** (PrivaPub's R7: the server's own emoji, `Services/EmojiService.cs`).
- **For everyone:** `GET /api/v1/custom_emojis` (no credentials; PrivaPub lists only those neither disabled nor kept out
of its picker) is read once a day and kept in localStorage (`CustomEmojiCache`, registered in `ClientJsonContext`);
a failed read keeps what was held and asks again five minutes later. An administrator's change in this client reads it
again at once (`EmojiService.Refresh`). An emoji out of the picker is still taken by PrivaPub when its `:shortcode:` is
written, but the client knows only the listed ones.
- **Rendering:** a local post, profile and announcement carry the emoji they name in `emojis` like a remote one, so
`CustomEmojis` shows them (the card, Profile, the announcements banner, NavMenu's persona name; NavMenu's other
personas, which `/clientapi` sends without emoji, take the server's list when the menu opens). A located post
(`/clientapi/post/nearby`, no `emojis`) takes the server's emoji its words name (`EmojiService.NamedIn`). A reaction
with a custom emoji shows its picture (Pleroma's `url`; an announcement's `url`, or the server's list for one the
stream tells of first) and is toggled as `:shortcode:` (`Content.ReactionOf`), which PrivaPub matches among the
post's own emoji for another server's; a notification of one says "reacted with" and its picture
(`emoji_url`, `data-notification-emoji`).
- **The picker** (`Components/EmojiPicker.razor`): a `DDropdown SheetOnNarrow` whose trigger is the caller's
(`data-emoji-picker-open`, set through `DDropdown.TriggerAttributes`), holding, only while open (`data-emoji-picker`),
a search (`input[data-emoji-picker-search]`, its `form` naming no form; Enter takes the first found), the caller's
quick choices (`Quick`), the server's emoji by category (none first) and common unicode ones by group
(`Helpers/UnicodeEmojis.cs`, single emoji only, searched by their Mastodon/GitHub shortcode and a few words). Each
choice is `data-emoji-choice` (the shortcode, or the character) and goes to `OnPick` as `:shortcode:` or the
character; it closes the picker first. A GIF shows its still picture under `prefers-reduced-motion`. The card's React
button (`data-react`) and the announcements banner's (`data-announcement-react`) react with it, after their quick
reactions; the composer's emoji button writes it where the caret is, replacing a selection, else at the end, a
shortcode with a space on each side where a letter would touch it (`AppStatusService.CaretOf` / `PlaceCaret`,
`main.js`'s `caretOf` and `placeCaret`).
- **Suggestions:** in the composer, `:` and two letters or more at the end of the words offer the matching emoji
(shortcodes starting with it first, eight at most) among the mention and hashtag suggestions, as
`data-emoji-suggestion="{shortcode}"`; one picked is written `:shortcode: `.
- **Administration** (`Components/Admin/EmojiPanel.razor`, `data-emoji-panel`, `data-admin-section="emoji"`;
`/clientapi/admin/emoji/list|insert|update|delete`): every emoji, disabled ones too, grouped by category
(`data-emoji-group="{category}"`, empty for none): each `data-emoji="{shortcode}"` with `data-emoji-state`
picker|hidden|disabled, its picture, size and badges, `data-emoji-edit` and `data-emoji-delete` (asked first: the
posts and profiles using it show `:shortcode:` as text). `data-emoji-new` and `data-emoji-edit` open
`data-emoji-dialog`: `input[data-emoji-shortcode]` (read-only when editing: a shortcode is the emoji's id elsewhere and
never changes), `input[data-emoji-category]` (a datalist of the categories in use), `data-emoji-picture` (a file input:
PNG, GIF, WebP or JPEG, 256 KB at most, checked here first; `data-emoji-preview` current|picked), the toggles
`data-emoji-picker` (in the picker) and `data-emoji-disabled`, `data-emoji-save`, `data-emoji-cancel`. The form is
`LocalEmojiForm` (validated by its annotations) sent as multipart with the picture streamed from the file as `image`,
the whole state on an update, the picture only when a new one is picked. PrivaPub's refusals show in the dialog,
their status on `data-emoji-error` (409 a shortcode taken in any case, 422 a picture refused, 400 an invalid form).
`data-emoji-reload` reads the list again. Note that `data-emoji-picker` names both the open picker (a `div`) and the
dialog's toggle (an `input`).
- **Statistics** (`Components/Admin/StatisticsPanel.razor`, `/clientapi/admin/statistics`, typed in ClientModels'
`Admin/ViewStatistics.cs`): the period's overview, the servers (by traffic, failures, latency or name), the crawler, the
ledger's events and the server's own days. Counted per server, never per account.
@@ -643,8 +688,8 @@ Never redeclare the base's members in a component.
D fields' parse errors (`DInputBase`, `DCalendar`) through the static `ErrorMessages.InvalidValue` / `InvalidDate`. A new
key needs its `public static string` property in `ErrorMessages.Designer.cs` too, or validation throws at runtime.
**Client storage** is localStorage (Blazored.LocalStorage, camelCase JSON): `AuthData`, `PublicCacheData`, `FeedFilter`
and the server cache `ServerInfos`, keyed by `nameof(Type)`. There is no IndexedDB: `ILoggingService` writes to the
**Client storage** is localStorage (Blazored.LocalStorage, camelCase JSON): `AuthData`, `PublicCacheData`, `FeedFilter`,
the server cache `ServerInfos` and the server's custom emoji `CustomEmojiCache`, keyed by `nameof(Type)`. There is no IndexedDB: `ILoggingService` writes to the
browser console only, and `main.js` deletes the database (`data`) where the client once kept its log, wherever a browser
still holds it.