R4 the client catches up: follow requests, followers, downvotes, dismissing, filtering a post, quote policies, statistics in detail
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:
thepraandClaude Opus 5.5 committed 2026-10-17 04:02:20 +02:00
1 parent b731d8c8c0
commit b8d7858acb
25 files changed
+1543 -61

No files matched your search

+44 -6
View File
@@ -41,12 +41,36 @@
<Composer GroupId="@(Filter.TimelineType is TimelineType.Groups ? Filter.GroupId : null)" OnPublished="Published" />
</PhoneSheet>
@if (Filter.TimelineType is TimelineType.Notifications && filteredRequests > 0)
@if (Filter.TimelineType is TimelineType.Notifications && (filteredRequests > 0 || followRequests > 0))
{
<a class="btn btn-sm self-center" href="notifications/filtered" data-filtered-requests>
<i class="ion-md-notifications-off"></i>
<span>@Localizer["{0} filtered, waiting for you", filteredRequests]</span>
</a>
<div class="flex flex-wrap justify-center gap-2">
@if (followRequests > 0)
{
<DButton Size="DSize.Sm" OnClick="() => isFollowRequestsOpen = true" data-follow-requests-open>
<i class="ion-md-person-add" aria-hidden="true"></i>
<span>@Localizer["{0} follow requests waiting", followRequests]</span>
</DButton>
}
@if (filteredRequests > 0)
{
<a class="btn btn-sm" href="notifications/filtered" data-filtered-requests>
<i class="ion-md-notifications-off"></i>
<span>@Localizer["{0} filtered, waiting for you", filteredRequests]</span>
</a>
}
</div>
}
@if (isFollowRequestsOpen)
{
<DModal Placement="DModalPlacement.ResponsiveBottom" IsOpen="true" IsOpenChanged="open => isFollowRequestsOpen = open" CloseText="@Localizer["Close"]"
data-follow-requests-dialog>
<Heading>@Localizer["Follow requests"]</Heading>
<ChildContent>
<div class="mt-3">
<FollowRequestsPanel OnAnswered="FollowRequestAnswered" />
</div>
</ChildContent>
</DModal>
}
@if (pending.Count > 0)
@@ -93,7 +117,7 @@
@* only the cards near the view are rendered: a feed of thousands stays as light as one of forty *@
<Virtualize Items="posts" Context="post" ItemSize="320" OverscanCount="8">
<Content @key="post.Key" Post="post" FilterIn="FilterIn" OnDeleted="Remove" OnReplied="Replied" OnTag="OpenTag" />
<Content @key="post.Key" Post="post" FilterIn="FilterIn" OnDeleted="Remove" OnDismissed="Dismissed" OnReplied="Replied" OnTag="OpenTag" />
</Virtualize>
<div id="@SentinelId" class="flex justify-center min-h-8 text-xs opacity-60">
@@ -127,6 +151,7 @@
[Inject] InstanceService InstanceService { get; set; }
[Inject] RequestStats RequestStats { get; set; }
[Inject] FeedMemory Memory { get; set; }
[Inject] AccountService AccountService { get; set; }
FeedFilter Filter { get; set; } = new();
List<FeedPost> posts = [];
@@ -136,6 +161,9 @@
int unreadNotifications;
// the requests the persona's notification policy filtered, shown on the Notifications tab
int filteredRequests;
// the accounts waiting for the persona to accept their follow (the first page), shown on the Notifications tab
int followRequests;
bool isFollowRequestsOpen;
int unreadConversations;
string maxId;
string minId;
@@ -431,8 +459,11 @@
await TimelineService.ReadNotificationsUpTo(newest, cancellationToken);
unreadNotifications = 0;
filteredRequests = (await TimelineService.NotificationPolicy(cancellationToken))?.Summary?.PendingRequestsCount ?? 0;
followRequests = (await AccountService.FollowRequests(null, cancellationToken)).Data is AccountPage requests ? requests.Accounts.Count : 0;
}
void FollowRequestAnswered(bool accepted) => followRequests = Math.Max(0, followRequests - 1);
/// <summary>The stream that tells this feed's new posts, or null for a feed PrivaPub does not stream.</summary>
static StreamSubscription StreamOf(FeedFilter filter) => filter.TimelineType switch
{
@@ -568,6 +599,13 @@
posts.RemoveAll(p => p.Key == post.Key || p.Status?.Id == post.Status?.Id);
}
// a dismissed notification is that item alone: others about the same post stay
void Dismissed(FeedPost post)
{
posts.RemoveAll(p => p.Key == post.Key);
pending.RemoveAll(p => p.Key == post.Key);
}
IEnumerable<NerdRow> FeedStats()
{
yield return NerdRow.Of(Localizer["items"], posts.Count);