147 lines
4.8 KiB
Plaintext
147 lines
4.8 KiB
Plaintext
@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);
|
||
|
|
}
|
||
|
|
}
|