R8 the Mastodon API made real in decePub: featured tags, notify, languages, recommendations, notes, suggestions, the direct stream
A profile shows the hashtags an account features as chips opening their feed; under another account's header is what the persona keeps of it for itself alone: for one it follows, a bell (follow with notify), the languages of its posts that reach the home feed (follow with languages[]) and Recommend (endorsements, private here), and for any account a private note saved on blur or with Save. Settings edits the persona's featured hashtags (added by name or from those used lately, 10 at most) and its reading preferences (/clientapi/avatar/private/preferences), which CascadingState keeps per persona and the card applies to sensitive media and content warnings. A hashtag's tab features it on the profile. Explore has Who to follow (/api/v2/suggestions, dismissed for good after asking); Administration has Staff picks. The Direct tab follows the direct stream's conversation events; status notifications show the post. The follows CSV carries notify and languages. 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
566b0e33ea
commit
d33697e3fc
28 files changed
+1784
-52
No files matched your search
@@ -214,10 +214,10 @@ all of them in every theme. When you add or change one:
|
||||
- `PublicCacheData` (page settings: theme colours, dark/light/system theming, language), persisted in localStorage;
|
||||
- the theme, applied through `AppStatusService.ApplyTheme` (`wwwroot/js/theme.js`);
|
||||
- `IsOnline` / `IsOffline`, polled every 10 seconds;
|
||||
- the session: `IsSignedIn`, `Personas` (the root's), `PersonaId` and `Persona` (its Mastodon `Account`), `IsModerator`
|
||||
and `IsAdmin` (the JWT's policies), with `LoadSession`, `SwitchPersona`, `PersonaDeleted` and `SignOut`. A persona
|
||||
change re-renders every subscriber; pages compare `PersonaId` and reload what belongs to the persona. The JWT is renewed
|
||||
hourly when needed;
|
||||
- the session: `IsSignedIn`, `Personas` (the root's), `PersonaId` and `Persona` (its Mastodon `Account`), `Preferences`
|
||||
(its reading preferences, see **Reading preferences**), `IsModerator` and `IsAdmin` (the JWT's policies), with
|
||||
`LoadSession`, `SwitchPersona`, `PersonaDeleted` and `SignOut`. A persona change re-renders every subscriber; pages
|
||||
compare `PersonaId` and reload what belongs to the persona. The JWT is renewed hourly when needed;
|
||||
- a persona the staff suspended (`ViewAvatar.AccountState` `Suspended` in `/clientapi/avatar/private/list`, `CanUse`) is
|
||||
never acted as: `LoadSession` skips it, `SwitchPersona` refuses it, and the switcher and Login's chooser show it
|
||||
disabled (`data-persona-state`). When PrivaPub refuses the current persona a token while the JWT is good (suspended
|
||||
@@ -307,11 +307,13 @@ Never redeclare the base's members in a component.
|
||||
reactions through Pleroma's `/api/v1/pleroma/statuses/:id/reactions`, joining and
|
||||
leaving remote events through `/api/privapub/v1/statuses/:id/join|leave`; located and group posts go through
|
||||
`/clientapi/post/insert`, and a located post's author deletes it through `/clientapi/post/delete`, its card's one action), `AccountService` (profiles, relationships, follow,
|
||||
block, mute, follow requests, the persona's followers and following and removing a follower, `/api/v2/search`, whose
|
||||
handles and URLs PrivaPub resolves), `ListService` (the
|
||||
block, mute, follow requests, the persona's followers and following and removing a follower, an account's featured
|
||||
hashtags, and what the persona keeps of an account for itself: notify, languages, endorsement and private note (see
|
||||
**Profiles and search**), `/api/v2/search`, whose handles and URLs PrivaPub resolves), `ListService` (the
|
||||
persona's lists and their members; members are only accounts it follows, found with `accounts/search?following=true`),
|
||||
`FilterService` (the persona's v2 filters, their keywords and filtered posts), `DiscoveryService` (hashtags and
|
||||
following them, trends, the directory), `PortabilityService` (Mastodon's CSV exports and imports), `AdminService`
|
||||
following and featuring them, the persona's featured hashtags and their suggestions, trends, the directory, who to
|
||||
follow), `PortabilityService` (Mastodon's CSV exports and imports), `AdminService`
|
||||
(`/clientapi/moderator`, the staff's log among it, and `/clientapi/admin`'s blocks, statistics and backups),
|
||||
`InstanceAdminService` (the server's own settings, thumbnail, announcements and relays: PrivaPub's
|
||||
`InstanceAdminController`), `ModerationService` (the staff's acts: accounts found, silenced, suspended and given back,
|
||||
@@ -360,6 +362,25 @@ Never redeclare the base's members in a component.
|
||||
violation (`ReportCategory.Violation`, offered while the server has rules) ticks the server's rules it breaks
|
||||
(`/api/v1/instance/rules`, sent as `rule_ids`; `data-report-rules`, a checkbox `data-report-rule="{id}"` each).
|
||||
Who liked or boosted a post opens from its card's menu (`Components/InteractedByDialog.razor`).
|
||||
The hashtags an account features (`GET /api/v1/accounts/:id/featured_tags`: a persona's own, an account elsewhere's as
|
||||
its server told them) are chips under its fields (`data-profile-featured-tags`, each a `button.badge`
|
||||
`data-profile-featured-tag="{name}"` opening that hashtag's feed tab, its count and last use as its title); on the
|
||||
persona's own profile, with a link to edit them (`data-profile-featured-tags-edit`, `settings?open=profile`, which opens
|
||||
Settings' Profile section). Under another account's header is what the persona keeps of it **for itself alone**
|
||||
(`data-profile-private`; the account is never told, and none of it federates):
|
||||
- for an account it follows, a bell (`data-profile-notify="on|off"`: follow again with `notify`, which gives the persona a
|
||||
`status` notification for each of its new posts), the languages of its posts that reach the home feed
|
||||
(`data-profile-languages`, a `DDropdown SheetOnNarrow` whose value is the codes kept, comma-separated, empty for every
|
||||
language; a `DCheckbox` `data-profile-language="{code}"` each, saved together with `data-profile-languages-save`,
|
||||
`data-profile-languages-all` for every language; follow again with `languages[]`, an empty value for none, since an
|
||||
empty list is every language) and its recommendation (`data-profile-endorse="on|off"`, `/accounts/:id/endorse|unendorse`:
|
||||
Mastodon's endorsements, which PrivaPub keeps private, the owner's default: worded Recommend / Keep in my
|
||||
recommendations, and said to be seen by the persona alone). Both follow calls send `reblogs` as the relationship has
|
||||
it, since a follow always sets it (`AccountService.SetNotify`, `SetLanguages`). Unfollowing drops the endorsement;
|
||||
- for any account, a private note (`textarea[data-profile-note]`, the relationship's `note`, `POST /accounts/:id/note`
|
||||
with `comment`, 2000 characters at most, empty takes it away), saved as it loses focus (a `focusout` on its wrapper) or
|
||||
with `data-profile-note-save`; `data-profile-note-saved` says it was. Settings' biography textarea in ProfileEditor is
|
||||
`data-profile-note` too, on another page.
|
||||
On the persona's own profile its counts are buttons (`data-profile-following`, `data-profile-followers`) opening the
|
||||
accounts it follows and its followers (`Components/AccountListDialog.razor`, 40 a page by the Link header): PrivaPub
|
||||
lists them to the persona alone (`hide_collections`), so another profile keeps plain counts. A follower is removed
|
||||
@@ -444,9 +465,19 @@ Never redeclare the base's members in a component.
|
||||
persona's profile (`ProfileEditor`:
|
||||
`update_credentials` as multipart, its words as written from `Account.Source`, pictures (streamed from the file,
|
||||
and removed with `DELETE /api/v1/profile/avatar|header`), fields, locked, bot,
|
||||
discoverable, indexable and posting defaults; the composer starts a new post at its default visibility), its mutes,
|
||||
blocks and blocked servers (`MutesAndBlocks`), and `DataTransfer`: follows, lists, blocks, mutes, blocked servers
|
||||
and bookmarks exported and imported as the CSV files Mastodon writes, so a persona moves between servers. An import
|
||||
discoverable, indexable and posting defaults; the composer starts a new post at its default visibility), its featured
|
||||
hashtags below it (`FeaturedTagsEditor`, `data-featured-tags`: `/api/v1/featured_tags`, public, so the persona's followers
|
||||
are told of each change; each `data-featured-tag="{name}"` with `data-featured-tag-remove`, removed at once since a
|
||||
hashtag can be featured again; added by `input[data-featured-tag-new]` (Enter or `data-featured-tag-add`, PrivaPub's 422
|
||||
shown under it) or from those it used lately, `/featured_tags/suggestions`, as `data-featured-tag-suggestion="{name}"`;
|
||||
at most the instance API's `max_featured_tags`, 10, counted in `data-featured-tags-count`), its reading preferences
|
||||
(`ReadingPreferencesEditor`, the Reading section: `data-preference-expand-media`, a select of default, show_all or
|
||||
hide_all, `data-preference-expand-spoilers`, a toggle, `data-preferences-save`, `data-preferences-reset`; `GET`/`POST
|
||||
/clientapi/avatar/private/preferences` with `PersonaPreferencesForm`, since Mastodon's API only reads them at
|
||||
`/api/v1/preferences`; see **Reading preferences**), its mutes,
|
||||
blocks and blocked servers (`MutesAndBlocks`), and `DataTransfer`: follows (with their boosts, notify and languages
|
||||
columns, each imported only when the file has it), lists, blocks, mutes, blocked servers and bookmarks exported and
|
||||
imported as the CSV files Mastodon writes, so a persona moves between servers. An import
|
||||
resolves each row through PrivaPub and acts one row at a time; it can be stopped. Above it, `PersonaArchivePanel`
|
||||
(`ArchiveService`, `/clientapi/persona/{id}/archive`; owner decision 2026-10-07): the persona's whole archive made on
|
||||
PrivaPub in Mastodon's layout and downloaded through a link; and an archive (its own, or Mastodon's) uploaded in pieces
|
||||
@@ -458,7 +489,17 @@ 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). Administrators also see Custom emoji (`emoji`, after Announcements; see **Custom emoji**).
|
||||
too). Administrators also see Custom emoji (`emoji`, after Announcements; see **Custom emoji**) and Staff picks
|
||||
(`staff-picks`, after Custom emoji).
|
||||
- **Staff picks** (`StaffPicksPanel`, `data-staff-picks`, `InstanceAdminService.StaffPicks`, `AddStaffPick`,
|
||||
`RemoveStaffPick`; `GET /clientapi/admin/staff-picks`, `POST /staff-picks/insert` with `StaffPickForm`, `POST
|
||||
/staff-picks/delete?accountId=`, each answering the picks as `List<ViewStaffPick>`): the accounts suggested first to
|
||||
every persona (Explore's Who to follow), in their order, each `data-staff-pick="{handle}"` (`data-staff-pick-id`,
|
||||
`data-staff-pick-shown` false for one deleted, suspended or silenced since, which is suggested to nobody) with
|
||||
`data-staff-pick-remove` (not asked: it can be picked again). Added from `input[data-staff-pick-search]`
|
||||
(`/api/v2/search`, never resolving, communities left out) as `data-staff-pick-option="{account id}"`; 40 at most
|
||||
(`data-staff-picks-count`). PrivaPub's 404 (no account it can suggest), 409 (picked already) and 422 (past 40) are
|
||||
toasts. The staff's log names them `staff_pick.insert` and `staff_pick.delete`.
|
||||
- **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,
|
||||
@@ -529,6 +570,13 @@ 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.
|
||||
- **Reading preferences** (PrivaPub's R8, `ViewPersonaPreferences`): how the persona reads what others post, read with it
|
||||
(`AuthService.PersonaPreferences` in `CascadingState.SwitchPersona`, PrivaPub's defaults when they can't be read) and
|
||||
kept as `CascadingState.Preferences`, replaced by `PreferencesChanged` after Settings saves them (and when Settings reads
|
||||
them changed from another device). The card applies them: media wait behind their button (`data-media-hidden`, its
|
||||
blurhash behind it) when marked sensitive (`default`), never (`show_all`) or always (`hide_all`, "Hidden media, show"),
|
||||
a blur filter's always (`Content.HidesMedia`); a content warning starts open with `ExpandSpoilers`. `Preferences` is in
|
||||
`Content.RenderKey`. `Models/Types/ExpandMedia.cs` maps the modes (`ExpandMediaModes`).
|
||||
- **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`);
|
||||
@@ -594,7 +642,9 @@ Never redeclare the base's members in a component.
|
||||
what notifications from people the persona does not follow, people who do not follow it, new accounts, private
|
||||
mentions it did not ask for and limited accounts become (accept, filter, drop; `/api/v2/notifications/policy`), and the
|
||||
filtered ones, one request per account, let in (with that account from then on) or dismissed. The Notifications tab
|
||||
links there while requests wait. A warning from the staff (`moderation_warning`, `Notification.ModerationWarning`) shows
|
||||
links there while requests wait. A `status` notification (an account the persona follows with notify posted, PrivaPub's
|
||||
R8) says "{0} posted" with the post (`data-notification="status"`, and `data-notification-status` holding the post's
|
||||
id). A warning from the staff (`moderation_warning`, `Notification.ModerationWarning`) shows
|
||||
what they did and what they wrote (`data-notification-warning="{action}"`, `data-notification-warning-text`). A
|
||||
notification is dismissed from its line (`data-notification-dismiss`,
|
||||
`TimelineService.DismissNotification`: `/api/v2/notifications/:group_key/dismiss`, its whole group, `ungrouped-{id}` for
|
||||
@@ -602,7 +652,14 @@ Never redeclare the base's members in a component.
|
||||
- **Scheduled posts** (`Pages/Scheduled.razor` at `/scheduled`): the composer's clock button gives a Mastodon API post a
|
||||
time (`scheduled_at`, at least five minutes ahead; located and group posts have none); the page moves or drops them.
|
||||
- **Explore** (`Pages/Explore.razor` at `/explore`): trending hashtags, posts and links (PrivaPub's, from public posts
|
||||
it holds), the directory, and the hashtags the persona follows. A hashtag's feed tab follows it too.
|
||||
it holds), the directory, who to follow, and the hashtags the persona follows. A hashtag's feed tab follows it too, and
|
||||
features it on the persona's profile (`data-tag-feature="on|off"`, `/api/v1/tags/:name/feature|unfeature`, the Tag's
|
||||
`featuring`; public, its followers are told).
|
||||
- **Who to follow** (`data-suggestions`, `GET /api/v2/suggestions`, 40): each `data-suggestion="{acct}"` with
|
||||
`data-suggestion-source` (staff: picked by the staff, past_interactions: the persona interacted with it, global:
|
||||
popular here; its `sources` as the badge's title), `data-suggestion-follow` (taken off the list once followed or asked)
|
||||
and `data-suggestion-dismiss` (`DELETE /api/v1/suggestions/:id`, for good, so asked first). PrivaPub never suggests the
|
||||
persona itself nor a sibling persona of its login.
|
||||
- **Mastodon DTOs** are client-side, in `Models/Mastodon/` (snake_case). `/clientapi` DTOs come from
|
||||
`PrivaPub.ClientModels`.
|
||||
- **Where a post is** (`InstanceService.Locate`, `Models/FeedPost.cs`): its own place (Pixelfed's, `privapub.place`),
|
||||
@@ -617,8 +674,12 @@ Never redeclare the base's members in a component.
|
||||
ten minutes, for the same feed of the same persona), so coming back from a thread or a profile shows it again at once
|
||||
without reading it anew. Older pages load as the end comes near (`wwwroot/js/feed.js`). Newer ones come through PrivaPub's
|
||||
streaming API (`Services/StreamingService.cs`: one WebSocket, the persona's token as its protocol, never in a URL)
|
||||
for home, lists, local, federated, hashtags and notifications, and wait behind a "new" button; a deletion or an edit
|
||||
it tells applies at once. Other feeds, and any feed while the stream is down, are polled every 45 s while visible.
|
||||
for home, lists, local, federated, hashtags, notifications and direct conversations, and wait behind a "new" button; a
|
||||
deletion or an edit it tells applies at once. The Direct tab follows the `direct` stream, whose `conversation` event
|
||||
(`StreamMessage.Conversation`) moves that conversation up: behind the "new" button, or at once at the top when the
|
||||
persona wrote its newest message (the message it just sent, shown alone until then, is replaced by it). Elsewhere the
|
||||
Direct tab's unread badge is still counted on every poll tick. Other feeds, and any feed while the stream is down, are
|
||||
polled every 45 s while visible.
|
||||
- **Announcements** (`Components/AnnouncementsBanner.razor`, above the tabs, `data-announcement-banner`): the ones
|
||||
shown now that the persona has not dismissed, one at a time (`data-announcement-previous`, `-next`,
|
||||
`-position`), dismissed (`data-announcement-dismiss`, `POST /api/v1/announcements/:id/dismiss`) and reacted to
|
||||
|
||||
Reference in new issue
Block a user