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

+147
View File
@@ -0,0 +1,147 @@
@inherits LocalizableComponentBase
@* The accounts waiting for the persona to accept their follow (/api/v1/follow_requests), newest first, each opening its
profile, accepted (authorize) or refused (reject) here; "Load more" follows the Link header's next page when PrivaPub
sends one. On the persona's own profile (hidden while nobody waits) and in a sheet from the Notifications tab. *@
@if (!HideWhenEmpty || IsLoading || requests.Count > 0)
{
<section class="flex flex-none flex-col gap-2 w-full" data-follow-requests>
@if (ShowHeading)
{
<h2 class="font-semibold text-sm inline-flex items-center gap-2"><i class="ion-md-person-add" aria-hidden="true"></i>@Localizer["Follow requests"]</h2>
}
@if (IsLoading)
{
<p class="flex items-center gap-2 text-sm"><DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />@Localizer["loading"]</p>
}
else if (requests.Count == 0)
{
<p class="text-sm opacity-70" data-follow-requests-empty>@Localizer["No follow request is waiting."]</p>
}
@foreach (var account in requests)
{
<div class="flex flex-wrap items-center gap-2 p-2 rounded-xl neo-raised neo-2xs min-w-0" data-follow-request="@account.Acct">
<a class="flex flex-1 items-center gap-3 min-w-0 pointer-coarse:min-h-11" href="@($"account/{Uri.EscapeDataString(account.Id)}")">
@* alt="": the name is next to it. *@
<img alt="" class="h-9 w-9 flex-none object-cover rounded-full" src="@(account.Avatar ?? "imgs/icon-192.png")" />
<span class="flex flex-col min-w-0">
<b class="truncate">@CustomEmojis.Text(account.Name, account.Emojis)</b>
<span class="text-xs opacity-70 truncate">@("@" + account.Acct)</span>
</span>
</a>
@if (answers.TryGetValue(account.Id, out var accepted))
{
<span class="text-xs opacity-70" data-follow-request-answer="@(accepted ? "accepted" : "refused")">
@(accepted ? Localizer["Accepted."] : Localizer["Refused."])
</span>
}
else
{
<div class="flex gap-2">
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="@(IsDefaultDisabled || busy.Contains(account.Id))" OnClick="() => Answer(account, true)"
data-follow-request-accept>
<i class="ion-md-checkmark" aria-hidden="true"></i><span>@Localizer["Accept"]</span>
</DButton>
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || busy.Contains(account.Id))" OnClick="() => Answer(account, false)"
data-follow-request-reject>
<i class="ion-md-close" aria-hidden="true"></i><span>@Localizer["Refuse"]</span>
</DButton>
</div>
}
</div>
}
@if (maxId is not null)
{
<DButton Size="DSize.Sm" class="self-center" Disabled="@(IsDefaultDisabled || isLoadingMore)" OnClick="LoadMore" data-follow-requests-more>
@if (isLoadingMore)
{
<DLoading Size="DSize.Xs" LabelText="@Localizer["loading"]" />
}
<span>@Localizer["Load more"]</span>
</DButton>
}
</section>
}
@code {
[Inject] AccountService AccountService { get; set; }
/// <summary>Renders nothing once read with nobody waiting (the persona's own profile).</summary>
[Parameter] public bool HideWhenEmpty { get; set; }
[Parameter] public bool ShowHeading { get; set; }
/// <summary>A request was answered: true when accepted.</summary>
[Parameter] public EventCallback<bool> OnAnswered { get; set; }
List<Account> requests = [];
// what was answered here, by account id: accepted (true) or refused
Dictionary<string, bool> answers = new();
HashSet<string> busy = [];
string maxId;
string loadedPersonaId;
bool isLoadingMore;
// The persona changed (NavMenu's switcher): its requests are another persona's.
protected override async Task OnParametersSetAsync()
{
if (CascadingState.PersonaId == loadedPersonaId)
return;
loadedPersonaId = CascadingState.PersonaId;
if (CascadingState.IsOffline)
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
await LoadData();
}
async Task LoadData()
{
IsLoading = true;
requests = [];
answers = new();
maxId = null;
await LoadPage();
IsLoading = false;
}
async Task LoadPage()
{
var result = await AccountService.FollowRequests(maxId);
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
var page = (AccountPage)result.Data;
var known = requests.Select(a => a.Id).ToHashSet();
requests.AddRange(page.Accounts.Where(a => known.Add(a.Id)));
maxId = page.MaxId;
}
async Task LoadMore()
{
isLoadingMore = true;
await LoadPage();
isLoadingMore = false;
}
async Task Answer(Account account, bool accept)
{
busy.Add(account.Id);
var result = await AccountService.Answer(account.Id, accept);
busy.Remove(account.Id);
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
answers[account.Id] = accept;
await OnAnswered.InvokeAsync(accept);
}
}