@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)] }; } }