@inherits LocalizableComponentBase
@* Accounts the staff moderate (/clientapi/moderator/accounts), for moderators and administrators: found by a name,
user@host or an actor's address (personas here, never their logins, and accounts elsewhere PrivaPub holds), and the
ones the staff silenced or suspended, most recently first, 50 at a time. Each is silenced, suspended or given back
with a note for the staff and, for a persona here, a warning it is told; an account elsewhere is told nothing. A
server silenced or suspended by a domain block holds its accounts back without their own silence, and says so. *@
@* its form attribute names no form, so Enter in the search submits nothing *@
@if (query is { Length: > 0 })
{
@if (isSearching)
{
@Localizer["loading"]
}
else if (found.Count == 0)
{
@Localizer["No account found."]
}
@foreach (var account in found)
{
@Row(account)
}
}
@Localizer["Silenced and suspended"]
@if (IsLoading)
{
@Localizer["loading"]
}
else if (failed)
{
@Localizer["The accounts could not be read."]
@Localizer["Retry"]
}
else if (moderated.Count == 0)
{
@Localizer["No account is silenced or suspended."]
}
@foreach (var account in moderated)
{
@Row(account)
}
@if (next is not null)
{
@if (isLoadingMore)
{
}
@Localizer["Load more"]
}
@if (acting is not null)
{
}
@code {
const int PageSize = 50;
const int SearchSize = 20;
[Inject] ModerationService ModerationService { get; set; }
string query;
List found = [];
List moderated = [];
// the offset of the moderated list's next page; null on the last
int? next;
bool failed;
bool isSearching;
bool isLoadingMore;
int searching;
// the account acted on and how, while the dialog asks
ViewModeratedAccount acting;
string action;
protected override async Task OnInitializedAsync()
{
if (CascadingState.IsOffline)
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
await LoadData();
IsLoading = false;
}
async Task LoadData()
{
var page = await ModerationService.ModeratedAccounts(0, PageSize);
failed = page is null;
moderated = page?.Items ?? [];
next = page?.Next;
}
async Task Reload()
{
IsLoading = true;
await LoadData();
IsLoading = false;
}
async Task More()
{
isLoadingMore = true;
var page = await ModerationService.ModeratedAccounts(next ?? 0, PageSize);
isLoadingMore = false;
if (page is null)
{
Toast.AddResponseError(Localizer["The accounts could not be read."]);
return;
}
var known = moderated.Select(a => a.Id).ToHashSet();
moderated.AddRange(page.Items.Where(a => known.Add(a.Id)));
next = page.Next;
}
async Task Search(string text)
{
query = text;
var lookup = ++searching;
if (string.IsNullOrWhiteSpace(text))
{
found = [];
isSearching = false;
return;
}
isSearching = true;
await Task.Delay(300);
if (lookup != searching)
return;
var page = await ModerationService.SearchAccounts(text.Trim(), SearchSize);
if (lookup != searching)
return;
found = page?.Items ?? [];
isSearching = false;
}
static string State(ViewModeratedAccount account) =>
account.SuspendedAt is not null ? "suspended" : account.SilencedAt is not null ? "silenced" : "normal";
RenderFragment Row(ViewModeratedAccount account) => @
@(account.DisplayName is { Length: > 0 } displayName ? displayName : account.UserName)
@("@" + account.Handle)
@(account.IsLocal ? Localizer["here"] : account.Domain)
@if (account.SilencedAt is { } silencedAt)
{
@Localizer["Silenced"]
}
@if (account.SuspendedAt is { } suspendedAt)
{
@Localizer["Suspended"]
}
@if (account.DomainSilenced)
{
@Localizer["its server is silenced"]
}
@if (account.DomainSuspended)
{
@Localizer["its server is suspended"]
}
@if (account.Note is { Length: > 0 } staffNote)
{
@Localizer["Staff note"] @staffNote
}
@if (account.SilencedAt is null)
{
@Localizer["Silence"]
}
else
{
@Localizer["Unsilence"]
}
@if (account.SuspendedAt is null)
{
@Localizer["Suspend"]
}
else
{
@Localizer["Unsuspend"]
}
;
string Name => acting?.DisplayName is { Length: > 0 } displayName ? displayName : "@" + acting?.Handle;
string Question => action switch
{
ModerationActions.Silence => Localizer["Silence {0}?", Name],
ModerationActions.Unsilence => Localizer["Unsilence {0}?", Name],
ModerationActions.Suspend => Localizer["Suspend {0}?", Name],
_ => Localizer["Unsuspend {0}?", Name]
};
string ActText => (action, acting?.IsLocal == true) switch
{
(ModerationActions.Silence, _) => Localizer["Its posts stay out of public timelines, hashtags, trends and search; its followers still read them."],
(ModerationActions.Suspend, true) => Localizer["The persona can't act or be seen here any more, and its sessions end; nothing new is told to other servers. It reads the warning once the suspension is lifted."],
(ModerationActions.Suspend, false) => Localizer["Nothing of it is shown here any more, what it sends is dropped and nothing is delivered to it. Its server is told nothing."],
(ModerationActions.Unsilence, _) => Localizer["Its posts are listed again."],
_ => Localizer["It is given back as it was, but the sessions a suspension ended."]
};
string ConfirmText => action switch
{
ModerationActions.Silence => Localizer["Silence"],
ModerationActions.Unsilence => Localizer["Unsilence"],
ModerationActions.Suspend => Localizer["Suspend"],
_ => Localizer["Unsuspend"]
};
void Ask(ViewModeratedAccount account, string act) => (acting, action) = (account, act);
void CloseAct() => (acting, action) = (null, null);
async Task Moderate(ActDialog.Answer answer)
{
var account = acting;
var act = action;
var result = await ModerationService.Moderate(new ModerateAccountForm
{
AccountId = account.Id,
Action = act,
Note = answer.Note,
Warning = account.IsLocal && act is ModerationActions.Silence or ModerationActions.Suspend ? answer.Warning : null
});
if (result.IsInvalid)
return result;
Changed(result.Data as ViewModeratedAccount ?? account);
Toast.AddResponseSuccess(act switch
{
ModerationActions.Silence => Localizer["{0} is silenced.", "@" + account.Handle],
ModerationActions.Unsilence => Localizer["{0} is no longer silenced.", "@" + account.Handle],
ModerationActions.Suspend => Localizer["{0} is suspended.", "@" + account.Handle],
_ => Localizer["{0} is no longer suspended.", "@" + account.Handle]
});
return result;
}
// the account as PrivaPub has it now, in both lists: one no longer held back leaves the moderated list, one newly held
// back joins it at the top
void Changed(ViewModeratedAccount account)
{
found = [.. found.Select(a => a.Id == account.Id ? account : a)];
var held = account.SilencedAt is not null || account.SuspendedAt is not null;
var listed = moderated.Any(a => a.Id == account.Id);
moderated = held switch
{
true when listed => [.. moderated.Select(a => a.Id == account.Id ? account : a)],
true => [account, .. moderated],
false => [.. moderated.Where(a => a.Id != account.Id)]
};
}
}