R4 the client catches up: follow requests, followers, downvotes, dismissing, filtering a post, quote policies, statistics in detail
Build / Build (push) Successful in 1m8s
Build / Build (push) Successful in 1m8s
Follow requests answered on the persona's own profile and from the Notifications tab; its followers and the accounts it follows listed there, a follower removed (asked first). Downvotes on threadiverse posts, after the public-vote notice. A notification dismissed from its line. "Filter this post" from a card's menu, and the Filters page showing the posts a filter holds. Who may quote the persona's own posts, and revoking a quote of one. Statistics: a server in detail with "describe again", the ledger's events, the server's own days. The composer deletes an upload taken out before posting. MediaType.Documents goes. 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
b731d8c8c0
commit
b8d7858acb
25 files changed
+1543
-61
No files matched your search
+136
-5
@@ -7,8 +7,11 @@
|
||||
quoted post (Compact) has none, since it is not an item of the feed. A CDN-fronted server's post has no point, only
|
||||
its CDN's group (data-cdn); data-post-at is when it was written, for the globe's Sun; data-uri is its ActivityPub id.
|
||||
The persona's filters apply where the page says the card is (FilterIn): a
|
||||
hide filter leaves it out, a warn filter folds it behind the filter's title, a blur filter blurs its media. Located posts have no Mastodon actions (PrivaPub keeps them off the Mastodon API); their author deletes them through /clientapi. Nerd stats are on
|
||||
the time, the author and its server's history, the place or the CDN, the media and the counts. *@
|
||||
hide filter leaves it out, a warn filter folds it behind the filter's title, a blur filter blurs its media; "Filter this
|
||||
post" puts it in one of them. Located posts have no Mastodon actions (PrivaPub keeps them off the Mastodon API); their
|
||||
author deletes them through /clientapi. A threadiverse post takes a downvote; the persona's own public post is given who
|
||||
may quote it, and a post quoting it can have its quote revoked; a notification is dismissed from its line. Nerd stats
|
||||
are on the time, the author and its server's history, the place or the CDN, the media and the counts. *@
|
||||
@if (!IsFilteredOut)
|
||||
{
|
||||
<article class="flex gap-3 w-full rounded-xl neo-raised @(Highlighted ? "neo-sm" : "neo-2xs") @CssContainer"
|
||||
@@ -49,6 +52,13 @@
|
||||
}
|
||||
<span>@NotificationText(notification)</span>
|
||||
<span class="opacity-60" title="@NerdRow.Time(notification.CreatedAt)">@notification.CreatedAt.GetPassedTime(Localizer)</span>
|
||||
@if (!Compact)
|
||||
{
|
||||
<DButton Size="DSize.Xs" class="ml-auto" Disabled="@(IsDefaultDisabled || isActing)" OnClick="DismissNotification" title="@Localizer["Dismiss"]"
|
||||
aria-label="@Localizer["Dismiss"]" data-notification-dismiss>
|
||||
<i class="ion-md-close" aria-hidden="true"></i>
|
||||
</DButton>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
else if (Post.BoostedBy is { } booster)
|
||||
@@ -283,11 +293,24 @@
|
||||
<span class="hidden md:inline">@status.FavouritesCount</span>
|
||||
}
|
||||
</DButton>
|
||||
@* a threadiverse post's downvotes: its upvotes are its favourites *@
|
||||
@if (status.Privapub?.Votes is { Down: > 0 } votes)
|
||||
@* a threadiverse post's downvotes: its upvotes are its favourites, and the persona's downvote replaces its
|
||||
favourite (on a phone, in More) *@
|
||||
@if (CanDownvote(status) && status.Privapub.Votes is { } votes)
|
||||
{
|
||||
<DButton Size="DSize.Sm" class="hidden md:inline-flex" Active="votes.Downvoted == true" Disabled="IsDefaultDisabled || isActing"
|
||||
OnClick="() => Downvote(status)" title="@(votes.Downvoted == true ? Localizer["Remove the downvote"] : Localizer["Downvote"])"
|
||||
aria-pressed="@(votes.Downvoted == true ? "true" : "false")" data-downvote="@(votes.Downvoted == true ? "on" : "off")">
|
||||
<i aria-hidden="true" class="ion-md-arrow-down text-lg @(votes.Downvoted == true ? "text-warning" : null)"></i>
|
||||
@if (votes.Down > 0)
|
||||
{
|
||||
<span class="tabular-nums" title="@Localizer["Downvotes"]" data-downvotes>@votes.Down</span>
|
||||
}
|
||||
</DButton>
|
||||
}
|
||||
else if (status.Privapub?.Votes is { Down: > 0 } ownVotes)
|
||||
{
|
||||
<span class="hidden md: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
|
||||
<i aria-hidden="true" class="ion-md-arrow-down"></i>@ownVotes.Down
|
||||
</span>
|
||||
}
|
||||
<DButton Size="DSize.Sm" class="hidden md:inline-flex" Active="status.Bookmarked" Disabled="IsDefaultDisabled || isActing"
|
||||
@@ -335,6 +358,18 @@
|
||||
<i aria-hidden="true" class="ion-md-bookmark text-base @(status.Bookmarked ? "text-warning" : null)"></i>
|
||||
<span>@Localizer["Bookmark"]</span>
|
||||
</DButton>
|
||||
@if (CanDownvote(status) && status.Privapub.Votes is { } phoneVotes)
|
||||
{
|
||||
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap md:hidden" Active="phoneVotes.Downvoted == true"
|
||||
Disabled="IsDefaultDisabled || isActing" OnClick="() => Downvote(status)" data-downvote-phone="@(phoneVotes.Downvoted == true ? "on" : "off")">
|
||||
<i aria-hidden="true" class="ion-md-arrow-down text-base @(phoneVotes.Downvoted == true ? "text-warning" : null)"></i>
|
||||
<span>@(phoneVotes.Downvoted == true ? Localizer["Remove the downvote"] : Localizer["Downvote"])</span>
|
||||
@if (phoneVotes.Down > 0)
|
||||
{
|
||||
<span class="opacity-70 tabular-nums">@phoneVotes.Down</span>
|
||||
}
|
||||
</DButton>
|
||||
}
|
||||
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap md:hidden" OnClick="OpenDetails" data-details>
|
||||
<i aria-hidden="true" class="ion-md-stats text-base"></i>
|
||||
<span>@Localizer["Details"]</span>
|
||||
@@ -376,6 +411,17 @@
|
||||
}
|
||||
@if (!IsOwn)
|
||||
{
|
||||
@if (QuotesOwnPost(status))
|
||||
{
|
||||
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled || isActing" OnClick="RevokeQuote" data-revoke-quote>
|
||||
<i aria-hidden="true" class="ion-md-quote text-base text-error"></i>
|
||||
<span>@Localizer["Revoke quote"]</span>
|
||||
</DButton>
|
||||
}
|
||||
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled" OnClick="@(() => isFilteringPost = true)" data-filter-post>
|
||||
<i aria-hidden="true" class="ion-md-funnel text-base"></i>
|
||||
<span>@Localizer["Filter this post"]</span>
|
||||
</DButton>
|
||||
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled" OnClick="@(() => isReporting = true)" data-report>
|
||||
<i aria-hidden="true" class="ion-md-flag text-base text-warning"></i>
|
||||
<span>@Localizer["Report"]</span>
|
||||
@@ -398,6 +444,11 @@
|
||||
<i aria-hidden="true" class="ion-md-pin text-base"></i>
|
||||
<span>@(status.Pinned ? Localizer["Unpin from profile"] : Localizer["Pin on profile"])</span>
|
||||
</DButton>
|
||||
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled || isActing"
|
||||
OnClick="@(() => isChoosingQuotePolicy = true)" data-quote-policy="@StatusService.QuotePolicyValue(StatusService.QuotePolicyOf(status))">
|
||||
<i aria-hidden="true" class="ion-md-quote text-base"></i>
|
||||
<span>@Localizer["Who may quote it"]</span>
|
||||
</DButton>
|
||||
}
|
||||
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled" OnClick="ToggleEdit" data-edit>
|
||||
<i aria-hidden="true" class="ion-md-create text-base"></i>
|
||||
@@ -461,6 +512,14 @@
|
||||
{
|
||||
<InteractedByDialog StatusId="@status.Id" Boosts="boosts" OnClosed="@(() => showInteractedBy = null)" />
|
||||
}
|
||||
@if (isFilteringPost)
|
||||
{
|
||||
<FilterPostDialog StatusId="@status.Id" OnClosed="@(() => isFilteringPost = false)" OnFiltered="Filtered" />
|
||||
}
|
||||
@if (isChoosingQuotePolicy)
|
||||
{
|
||||
<QuotePolicyDialog Status="status" OnClosed="@(() => isChoosingQuotePolicy = false)" OnChanged="QuotePolicyChanged" />
|
||||
}
|
||||
@if (isHistoryOpen)
|
||||
{
|
||||
<DModal Placement="DModalPlacement.ResponsiveBottom" IsOpen="true" IsOpenChanged="HistoryClosed" CloseText="@Localizer["Close"]" data-history-list>
|
||||
@@ -521,6 +580,8 @@
|
||||
|
||||
[Parameter, EditorRequired] public FeedPost Post { get; set; }
|
||||
[Parameter] public EventCallback<FeedPost> OnDeleted { get; set; }
|
||||
/// <summary>Its notification was dismissed: the page takes the item out.</summary>
|
||||
[Parameter] public EventCallback<FeedPost> OnDismissed { get; set; }
|
||||
[Parameter] public EventCallback<FeedPost> OnReplied { get; set; }
|
||||
[Parameter] public EventCallback<string> OnTag { get; set; }
|
||||
[Parameter] public string CssContainer { get; set; }
|
||||
@@ -547,6 +608,8 @@
|
||||
bool isReadingServer;
|
||||
bool isHistoryOpen;
|
||||
bool isReporting;
|
||||
bool isFilteringPost;
|
||||
bool isChoosingQuotePolicy;
|
||||
// who liked it (false) or boosted it (true), when that list is open
|
||||
bool? showInteractedBy;
|
||||
List<StatusEdit> history = [];
|
||||
@@ -824,6 +887,74 @@
|
||||
Post.Status = updated.Reblog ?? updated;
|
||||
}
|
||||
|
||||
// a threadiverse post (Lemmy, PieFed, Mbin, NodeBB…) takes downvotes. PrivaPub sends votes on every post, so the card
|
||||
// offers one where they mean something: the author's server is a threadiverse one, the post is a community's Page, or it
|
||||
// has downvotes already. Not on the persona's own post.
|
||||
bool CanDownvote(Status status) => !Compact && !Post.Located && !IsOwn && status.Privapub?.Votes is { } votes
|
||||
&& (votes.Down > 0 || votes.Downvoted == true || status.Privapub.ObjectType == "Page" || IsThreadiverse(Server?.Software));
|
||||
|
||||
static bool IsThreadiverse(string software) => software?.ToLowerInvariant() is "lemmy" or "piefed" or "mbin" or "kbin" or "sublinks" or "lotide" or "nodebb";
|
||||
|
||||
// a vote is public, like a reaction: the persona is told so once, before its first
|
||||
async Task Downvote(Status status)
|
||||
{
|
||||
var on = status.Privapub?.Votes?.Downvoted != true;
|
||||
if (on && !await Notices.Confirm(CascadingState.PersonaId))
|
||||
return;
|
||||
await Act(on ? StatusAction.Downvote : StatusAction.Undownvote);
|
||||
}
|
||||
|
||||
// the notification goes, its whole group with it (/api/v2/notifications/:group_key/dismiss)
|
||||
async Task DismissNotification()
|
||||
{
|
||||
var notification = Post.Notification;
|
||||
isActing = true;
|
||||
var result = await TimelineService.DismissNotification(notification.GroupKey ?? $"ungrouped-{notification.Id}");
|
||||
isActing = false;
|
||||
if (result.IsInvalid)
|
||||
{
|
||||
Toast.AddResponseError(result);
|
||||
return;
|
||||
}
|
||||
|
||||
await OnDismissed.InvokeAsync(Post);
|
||||
}
|
||||
|
||||
// in a filter now: the post is read again, so the filter applies here at once
|
||||
async Task Filtered()
|
||||
{
|
||||
isFilteringPost = false;
|
||||
if (await StatusService.Get(Status.Id) is { } filtered)
|
||||
Post.Status = filtered.Reblog ?? filtered;
|
||||
}
|
||||
|
||||
void QuotePolicyChanged(Status updated)
|
||||
{
|
||||
isChoosingQuotePolicy = false;
|
||||
Post.Status = updated.Reblog ?? updated;
|
||||
}
|
||||
|
||||
// someone else's post quoting the persona's own: the persona may take its permission back
|
||||
bool QuotesOwnPost(Status status) => !IsOwn && CascadingState.PersonaId is { } personaId && status.Quote?.QuotedStatus?.Account?.Id == personaId;
|
||||
|
||||
async Task RevokeQuote()
|
||||
{
|
||||
if (!await Confirm.Ask(Localizer["Revoke this quote of your post?"], Localizer["Your post no longer shows in it."], Localizer["Revoke quote"], true))
|
||||
return;
|
||||
isActing = true;
|
||||
var result = await StatusService.RevokeQuote(Status.Quote.QuotedStatus.Id, Status.Id);
|
||||
isActing = false;
|
||||
if (result.IsInvalid)
|
||||
{
|
||||
Toast.AddResponseError(result);
|
||||
return;
|
||||
}
|
||||
|
||||
var updated = (Status)result.Data;
|
||||
Post.Status = updated.Reblog ?? updated;
|
||||
Toast.AddResponseSuccess(Localizer["Quote revoked."]);
|
||||
}
|
||||
|
||||
[Inject] AccountService AccountService { get; set; }
|
||||
[Inject] PublicNoticeService Notices { get; set; }
|
||||
|
||||
|
||||
Reference in new issue
Block a user