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
137 lines
4.6 KiB
Plaintext
137 lines
4.6 KiB
Plaintext
@inherits LocalizableComponentBase
|
|
|
|
@* The persona's own followers or the accounts it follows (/api/v1/accounts/:id/followers, /following), 40 a page by the
|
|
Link header, each opening its profile. PrivaPub lists them to the persona alone (hide_collections), so only the
|
|
persona's own profile opens this. A follower can be removed (remove_from_followers), asked first. *@
|
|
<DModal Placement="DModalPlacement.ResponsiveBottom" IsOpen="true" IsOpenChanged="Closed" CloseText="@Localizer["Close"]"
|
|
data-account-list="@(Followers ? "followers" : "following")">
|
|
<Heading><span class="capitalize">@(Followers ? Localizer["followers"] : Localizer["following"])</span></Heading>
|
|
<ChildContent>
|
|
@if (IsLoading)
|
|
{
|
|
<p class="flex items-center gap-2 mt-3"><DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />@Localizer["loading"]</p>
|
|
}
|
|
else if (accounts.Count == 0)
|
|
{
|
|
<p class="text-sm opacity-70 mt-3" data-account-list-empty>@Localizer["Nobody yet."]</p>
|
|
}
|
|
<div class="flex flex-col gap-2 mt-3">
|
|
@foreach (var account in accounts)
|
|
{
|
|
<div class="flex flex-wrap items-center gap-2 p-2 rounded-xl neo-raised neo-2xs min-w-0" data-account="@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 (Followers)
|
|
{
|
|
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || busy.Contains(account.Id))" OnClick="() => RemoveFollower(account)"
|
|
data-follower-remove>
|
|
<i class="ion-md-person text-error" aria-hidden="true"></i><span>@Localizer["Remove from followers"]</span>
|
|
</DButton>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
@if (maxId is not null)
|
|
{
|
|
<div class="flex justify-center mt-3">
|
|
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isLoadingMore)" OnClick="LoadMore" data-account-list-more>
|
|
@if (isLoadingMore)
|
|
{
|
|
<DLoading Size="DSize.Xs" LabelText="@Localizer["loading"]" />
|
|
}
|
|
<span>@Localizer["Load more"]</span>
|
|
</DButton>
|
|
</div>
|
|
}
|
|
</ChildContent>
|
|
</DModal>
|
|
|
|
@code {
|
|
[Inject] AccountService AccountService { get; set; }
|
|
[Inject] ConfirmService Confirm { get; set; }
|
|
|
|
/// <summary>The persona's own account id.</summary>
|
|
[Parameter, EditorRequired] public string AccountId { get; set; }
|
|
/// <summary>Its followers, rather than the accounts it follows.</summary>
|
|
[Parameter] public bool Followers { get; set; }
|
|
[Parameter] public EventCallback OnClosed { get; set; }
|
|
/// <summary>A follower was removed.</summary>
|
|
[Parameter] public EventCallback<Account> OnRemoved { get; set; }
|
|
|
|
List<Account> accounts = [];
|
|
HashSet<string> busy = [];
|
|
string maxId;
|
|
bool isLoadingMore;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
if (CascadingState.IsOffline)
|
|
{
|
|
WhenOnline(LoadData);
|
|
StartOnlinePolling();
|
|
IsLoading = false;
|
|
return;
|
|
}
|
|
|
|
await LoadData();
|
|
IsLoading = false;
|
|
}
|
|
|
|
async Task LoadData()
|
|
{
|
|
accounts = [];
|
|
maxId = null;
|
|
await LoadPage();
|
|
}
|
|
|
|
async Task LoadPage()
|
|
{
|
|
var result = Followers ? await AccountService.Followers(AccountId, maxId) : await AccountService.Following(AccountId, maxId);
|
|
if (result.IsInvalid)
|
|
{
|
|
Toast.AddResponseError(result);
|
|
return;
|
|
}
|
|
|
|
var page = (AccountPage)result.Data;
|
|
var known = accounts.Select(a => a.Id).ToHashSet();
|
|
accounts.AddRange(page.Accounts.Where(a => known.Add(a.Id)));
|
|
maxId = page.MaxId;
|
|
}
|
|
|
|
async Task LoadMore()
|
|
{
|
|
isLoadingMore = true;
|
|
await LoadPage();
|
|
isLoadingMore = false;
|
|
}
|
|
|
|
async Task RemoveFollower(Account account)
|
|
{
|
|
var name = account.Name ?? account.Acct;
|
|
if (!await Confirm.Ask(Localizer["Remove {0} from your followers?", name], Localizer["They no longer see your followers-only posts, and may ask to follow you again."],
|
|
Localizer["Remove from followers"], true))
|
|
return;
|
|
busy.Add(account.Id);
|
|
var result = await AccountService.RemoveFromFollowers(account.Id);
|
|
busy.Remove(account.Id);
|
|
if (result.IsInvalid)
|
|
{
|
|
Toast.AddResponseError(result);
|
|
return;
|
|
}
|
|
|
|
accounts.Remove(account);
|
|
Toast.AddResponseSuccess(Localizer["{0} removed from your followers.", name]);
|
|
await OnRemoved.InvokeAsync(account);
|
|
}
|
|
|
|
Task Closed(bool open) => open ? Task.CompletedTask : OnClosed.InvokeAsync();
|
|
}
|