502 lines
20 KiB
Plaintext
502 lines
20 KiB
Plaintext
@inherits LocalizableComponentBase
|
|||
|
|
|
||
|
|
@* The logins, for administrators (/clientapi/admin/roots): the one place a login is shown with its personas and the
|
||
|
|
groups they own. Newest first, 50 at a time, found by a login's name or a persona's. A login's role (user, moderator,
|
||
|
|
administrator), its ban, its two-factor sign-in and its deletion are asked first (ConfirmService); never one's own,
|
||
|
|
and never the last administrator's role (PrivaPub answers 409). A persona is silenced, suspended or given back with a
|
||
|
|
note for the staff and a warning it is told; a persona or a group is deleted with the administrator's own password.
|
||
|
|
Every act is kept in the staff's log. *@
|
||
|
|
<div class="flex flex-col gap-3 w-full" data-users>
|
||
|
|
<div class="flex flex-wrap items-center gap-2 pointer-coarse:gap-2">
|
||
|
|
@* its form attribute names no form, so Enter in the search submits nothing *@
|
||
|
|
<DInput TValue="string" class="flex-1 min-w-48" Value="query" ValueChanged="Search" ValueExpression="() => query" Immediate="true" autocomplete="off"
|
||
|
|
form="users-search" placeholder="@Localizer["A login's name, or one of its personas'"]" aria-label="@Localizer["Search logins"]" data-users-search>
|
||
|
|
<Prefix><i class="ion-md-search" aria-hidden="true"></i></Prefix>
|
||
|
|
</DInput>
|
||
|
|
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled || isLoadingMore" OnClick="Reload" data-users-reload>
|
||
|
|
<i class="ion-md-refresh" aria-hidden="true"></i><span>@Localizer["Read again"]</span>
|
||
|
|
</DButton>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@if (IsLoading)
|
||
|
|
{
|
||
|
|
<p class="flex w-full items-center justify-center gap-2">
|
||
|
|
<DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />
|
||
|
|
@Localizer["loading"]
|
||
|
|
</p>
|
||
|
|
}
|
||
|
|
else if (failed)
|
||
|
|
{
|
||
|
|
<div class="flex flex-wrap items-center gap-2">
|
||
|
|
<p class="text-sm text-error">@Localizer["The logins could not be read."]</p>
|
||
|
|
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled" OnClick="Reload">@Localizer["Retry"]</DButton>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
else if (roots.Count == 0)
|
||
|
|
{
|
||
|
|
<p class="text-sm opacity-70" data-users-empty>@Localizer["No login found."]</p>
|
||
|
|
}
|
||
|
|
|
||
|
|
<div class="flex flex-col gap-2">
|
||
|
|
@foreach (var root in roots)
|
||
|
|
{
|
||
|
|
<article @key="root.Id" class="flex flex-col gap-2 p-2 md:p-3 rounded-lg neo-raised neo-2xs" data-user="@root.UserName" data-user-role="@root.Role"
|
||
|
|
data-user-banned="@(root.IsBanned ? "true" : "false")">
|
||
|
|
<div class="flex flex-wrap items-center gap-2 min-w-0">
|
||
|
|
<i class="ion-md-key opacity-60" aria-hidden="true"></i>
|
||
|
|
<b class="truncate min-w-0">@root.UserName</b>
|
||
|
|
@if (root.IsYou)
|
||
|
|
{
|
||
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Primary" data-user-you>@Localizer["you"]</DBadge>
|
||
|
|
}
|
||
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="@RoleColor(root.Role)">@RoleName(root.Role)</DBadge>
|
||
|
|
@if (root.IsBanned)
|
||
|
|
{
|
||
|
|
<DBadge Size="DSize.Sm" Color="DColor.Error" data-user-banned-badge>@Localizer["banned"]</DBadge>
|
||
|
|
}
|
||
|
|
@if (root.HasTotp)
|
||
|
|
{
|
||
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Success" data-user-totp>@Localizer["two-factor"]</DBadge>
|
||
|
|
}
|
||
|
|
@if (!root.HasEmail)
|
||
|
|
{
|
||
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Outline" data-user-email="none">@Localizer["no email address"]</DBadge>
|
||
|
|
}
|
||
|
|
else if (root.IsEmailValidated)
|
||
|
|
{
|
||
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Success" data-user-email="verified">@Localizer["email verified"]</DBadge>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Warning" data-user-email="unverified">@Localizer["email not verified"]</DBadge>
|
||
|
|
}
|
||
|
|
<span class="flex-1"></span>
|
||
|
|
<span class="text-xs opacity-70" title="@root.CreatedAt.ToLocalTime().ToString(SUtility.dateTimeFormatExt)">@root.CreatedAt.GetPassedTime(Localizer)</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex flex-wrap items-center gap-2 pointer-coarse:gap-2">
|
||
|
|
<DButton Size="DSize.Sm" Active="@opened.Contains(root.Id)" OnClick="() => Toggle(root)" aria-expanded="@(opened.Contains(root.Id) ? "true" : "false")"
|
||
|
|
data-user-open>
|
||
|
|
<i class="@(opened.Contains(root.Id) ? "ion-md-arrow-dropup" : "ion-md-arrow-dropdown")" aria-hidden="true"></i>
|
||
|
|
<span>@Localizer["Personas: {0}", root.Personas.Count]</span>
|
||
|
|
</DButton>
|
||
|
|
<span class="flex-1"></span>
|
||
|
|
@if (!root.IsYou)
|
||
|
|
{
|
||
|
|
<DSelect @key="@($"{root.Id}:{roleRenders}")" TValue="string" Size="DSize.Sm" Value="@root.Role" ValueChanged="role => ChangeRole(root, role)"
|
||
|
|
ValueExpression="() => root.Role" disabled="@(IsDefaultDisabled || busy.Contains(root.Id))" aria-label="@Localizer["Role"]"
|
||
|
|
data-user-role-select>
|
||
|
|
<option value="user">@RoleName("user")</option>
|
||
|
|
<option value="moderator">@RoleName("moderator")</option>
|
||
|
|
<option value="admin">@RoleName("admin")</option>
|
||
|
|
</DSelect>
|
||
|
|
@if (root.IsBanned)
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || busy.Contains(root.Id))" OnClick="() => Unban(root)" data-user-unban>
|
||
|
|
<i class="ion-md-undo" aria-hidden="true"></i><span>@Localizer["Unban"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || busy.Contains(root.Id))" OnClick="() => Ban(root)" data-user-ban>
|
||
|
|
<i class="ion-md-hand text-warning" aria-hidden="true"></i><span>@Localizer["Ban"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
@if (root.HasTotp)
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || busy.Contains(root.Id))" OnClick="() => ResetTotp(root)" data-user-totp-reset>
|
||
|
|
<i class="ion-md-unlock" aria-hidden="true"></i><span>@Localizer["Reset two-factor"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || busy.Contains(root.Id))" OnClick="() => DeleteRoot(root)" data-user-delete>
|
||
|
|
<i class="ion-md-trash text-error" aria-hidden="true"></i><span>@Localizer["Delete the login"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@if (opened.Contains(root.Id))
|
||
|
|
{
|
||
|
|
<div class="flex flex-col gap-2 ps-2 md:ps-4 border-s-2 border-base-300" data-user-personas>
|
||
|
|
@if (root.Personas.Count == 0)
|
||
|
|
{
|
||
|
|
<p class="text-xs opacity-70">@Localizer["This login has no persona."]</p>
|
||
|
|
}
|
||
|
|
@foreach (var persona in root.Personas)
|
||
|
|
{
|
||
|
|
<div class="flex flex-col gap-2 p-2 rounded-lg neo-inset neo-2xs" data-persona="@persona.UserName" data-persona-state="@State(persona)">
|
||
|
|
<div class="flex flex-wrap items-center gap-2 min-w-0">
|
||
|
|
<span class="flex flex-col min-w-0 flex-1">
|
||
|
|
<a class="font-semibold truncate link link-hover pointer-coarse:py-3.5" href="@($"account/{Uri.EscapeDataString(persona.Id)}")">
|
||
|
|
@(persona.DisplayName is { Length: > 0 } displayName ? displayName : persona.UserName)
|
||
|
|
</a>
|
||
|
|
<span class="text-xs opacity-70 truncate">@("@" + persona.Handle)</span>
|
||
|
|
</span>
|
||
|
|
@if (persona.SilencedAt is { } silencedAt)
|
||
|
|
{
|
||
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Warning" title="@silencedAt.ToLocalTime().ToString(SUtility.dateTimeFormatExt)">
|
||
|
|
@Localizer["Silenced"]
|
||
|
|
</DBadge>
|
||
|
|
}
|
||
|
|
@if (persona.SuspendedAt is { } suspendedAt)
|
||
|
|
{
|
||
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Error" title="@suspendedAt.ToLocalTime().ToString(SUtility.dateTimeFormatExt)">
|
||
|
|
@Localizer["Suspended"]
|
||
|
|
</DBadge>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<div class="flex flex-wrap justify-end gap-2 pointer-coarse:gap-2">
|
||
|
|
@if (persona.SilencedAt is null)
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled" OnClick="() => Ask(root, persona, null, ModerationActions.Silence)" data-persona-silence>
|
||
|
|
<i class="ion-md-volume-off text-warning" aria-hidden="true"></i><span>@Localizer["Silence"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled" OnClick="() => Ask(root, persona, null, ModerationActions.Unsilence)" data-persona-unsilence>
|
||
|
|
<i class="ion-md-volume-high" aria-hidden="true"></i><span>@Localizer["Unsilence"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
@if (persona.SuspendedAt is null)
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled" OnClick="() => Ask(root, persona, null, ModerationActions.Suspend)" data-persona-suspend>
|
||
|
|
<i class="ion-md-remove-circle text-error" aria-hidden="true"></i><span>@Localizer["Suspend"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled" OnClick="() => Ask(root, persona, null, ModerationActions.Unsuspend)" data-persona-unsuspend>
|
||
|
|
<i class="ion-md-undo" aria-hidden="true"></i><span>@Localizer["Unsuspend"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled" OnClick="() => Ask(root, persona, null, PersonaDelete)" data-persona-delete>
|
||
|
|
<i class="ion-md-trash text-error" aria-hidden="true"></i><span>@Localizer["Delete"]</span>
|
||
|
|
</DButton>
|
||
|
|
</div>
|
||
|
|
@foreach (var group in persona.Groups)
|
||
|
|
{
|
||
|
|
<div class="flex flex-wrap items-center gap-2 min-w-0 text-sm" data-group="@group.UserName">
|
||
|
|
<i class="@(group.IsCommunity ? "ion-md-globe" : "ion-md-lock") opacity-70" aria-hidden="true"></i>
|
||
|
|
<span class="flex-1 min-w-0 truncate">
|
||
|
|
<b>@(group.Name ?? group.UserName)</b> <span class="text-xs opacity-70">@("@" + group.UserName)</span>
|
||
|
|
</span>
|
||
|
|
<span class="text-xs tabular-nums opacity-70" title="@Localizer["Members"]"><i class="ion-md-person" aria-hidden="true"></i> @group.MembersCount</span>
|
||
|
|
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled" OnClick="() => Ask(root, persona, group, GroupDelete)" title="@Localizer["Delete the group"]"
|
||
|
|
aria-label="@Localizer["Delete the group"]" data-group-delete>
|
||
|
|
<i class="ion-md-trash text-error" aria-hidden="true"></i><span class="hidden sm:inline">@Localizer["Delete the group"]</span>
|
||
|
|
</DButton>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
</article>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@if (next is not null)
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Sm" class="self-center" Disabled="IsDefaultDisabled || isLoadingMore" OnClick="More" data-users-more>
|
||
|
|
@if (isLoadingMore)
|
||
|
|
{
|
||
|
|
<DLoading Size="DSize.Sm" LabelText="@Localizer["loading"]" />
|
||
|
|
}
|
||
|
|
<span>@Localizer["Load more"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@if (act is not null)
|
||
|
|
{
|
||
|
|
<ActDialog Question="@Question" Text="@ActText" ConfirmText="@ConfirmText" Danger="@(act is not (ModerationActions.Unsilence or ModerationActions.Unsuspend))"
|
||
|
|
Act="@act" Staff="true" AskNote="true" AskWarning="@(act is ModerationActions.Silence or ModerationActions.Suspend)"
|
||
|
|
AskPassword="@(act is PersonaDelete or GroupDelete)" OnConfirm="Act" OnClosed="CloseAct" />
|
||
|
|
}
|
||
|
|
|
||
|
|
@code {
|
||
|
|
const int PageSize = 50;
|
||
|
|
const string PersonaDelete = "persona-delete";
|
||
|
|
const string GroupDelete = "group-delete";
|
||
|
|
|
||
|
|
[Inject] ModerationService ModerationService { get; set; }
|
||
|
|
[Inject] ConfirmService Confirm { get; set; }
|
||
|
|
|
||
|
|
string query;
|
||
|
|
List<ViewAdminRoot> roots = [];
|
||
|
|
// the next page's `before`; null on the last
|
||
|
|
string next;
|
||
|
|
bool failed;
|
||
|
|
bool isLoadingMore;
|
||
|
|
int searching;
|
||
|
|
// the logins whose personas are shown
|
||
|
|
HashSet<string> opened = [];
|
||
|
|
// the logins being changed
|
||
|
|
HashSet<string> busy = [];
|
||
|
|
// rebuilds the role selects, so one whose change was cancelled shows the role again
|
||
|
|
int roleRenders;
|
||
|
|
// the act the dialog asks about (a persona's silence or suspension, a persona's or a group's deletion) and its targets
|
||
|
|
string act;
|
||
|
|
ViewAdminRoot actingRoot;
|
||
|
|
ViewAdminPersona actingPersona;
|
||
|
|
ViewAdminGroup actingGroup;
|
||
|
|
|
||
|
|
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.Roots(query?.Trim(), null, PageSize);
|
||
|
|
failed = page is null;
|
||
|
|
roots = page?.Items ?? [];
|
||
|
|
next = page?.Next;
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task Reload()
|
||
|
|
{
|
||
|
|
IsLoading = true;
|
||
|
|
await LoadData();
|
||
|
|
IsLoading = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task Search(string text)
|
||
|
|
{
|
||
|
|
query = text;
|
||
|
|
var lookup = ++searching;
|
||
|
|
await Task.Delay(300);
|
||
|
|
if (lookup != searching)
|
||
|
|
return;
|
||
|
|
IsLoading = true;
|
||
|
|
await LoadData();
|
||
|
|
if (lookup != searching)
|
||
|
|
return;
|
||
|
|
IsLoading = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task More()
|
||
|
|
{
|
||
|
|
isLoadingMore = true;
|
||
|
|
var page = await ModerationService.Roots(query?.Trim(), next, PageSize);
|
||
|
|
isLoadingMore = false;
|
||
|
|
if (page is null)
|
||
|
|
{
|
||
|
|
Toast.AddResponseError(Localizer["The logins could not be read."]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var known = roots.Select(r => r.Id).ToHashSet();
|
||
|
|
roots.AddRange(page.Items.Where(r => known.Add(r.Id)));
|
||
|
|
next = page.Next;
|
||
|
|
}
|
||
|
|
|
||
|
|
void Toggle(ViewAdminRoot root)
|
||
|
|
{
|
||
|
|
if (!opened.Remove(root.Id))
|
||
|
|
opened.Add(root.Id);
|
||
|
|
}
|
||
|
|
|
||
|
|
static string State(ViewAdminPersona persona) =>
|
||
|
|
persona.SuspendedAt is not null ? "suspended" : persona.SilencedAt is not null ? "silenced" : "normal";
|
||
|
|
|
||
|
|
string RoleName(string role) => role switch
|
||
|
|
{
|
||
|
|
"admin" => Localizer["administrator"],
|
||
|
|
"moderator" => Localizer["moderator"],
|
||
|
|
_ => Localizer["user"]
|
||
|
|
};
|
||
|
|
|
||
|
|
static DColor RoleColor(string role) => role switch
|
||
|
|
{
|
||
|
|
"admin" => DColor.Secondary,
|
||
|
|
"moderator" => DColor.Info,
|
||
|
|
_ => DColor.Neutral
|
||
|
|
};
|
||
|
|
|
||
|
|
// the login as PrivaPub has it now, in its place; one gone (deleted) leaves the list
|
||
|
|
async Task Refresh(ViewAdminRoot root)
|
||
|
|
{
|
||
|
|
var now = await ModerationService.Root(root.Id);
|
||
|
|
roots = now is null ? [.. roots.Where(r => r.Id != root.Id)] : [.. roots.Select(r => r.Id == root.Id ? now : r)];
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task ChangeRole(ViewAdminRoot root, string role)
|
||
|
|
{
|
||
|
|
var (question, text) = role switch
|
||
|
|
{
|
||
|
|
"admin" => (Localizer["Make {0} an administrator?", root.UserName],
|
||
|
|
Localizer["Administrators do everything moderators do, and change logins, the server's settings, its relays and its backups."]),
|
||
|
|
"moderator" => (Localizer["Make {0} a moderator?", root.UserName],
|
||
|
|
Localizer["Moderators handle reports, silence and suspend accounts and remove posts; never logins or the server's settings."]),
|
||
|
|
_ => (Localizer["Make {0} a user?", root.UserName], Localizer["It keeps its personas, and loses what the staff do."])
|
||
|
|
};
|
||
|
|
if (!await Confirm.Ask(question, text, Localizer["Change the role"], true))
|
||
|
|
{
|
||
|
|
roleRenders++;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
busy.Add(root.Id);
|
||
|
|
var result = await ModerationService.SetRole(root.Id, role);
|
||
|
|
busy.Remove(root.Id);
|
||
|
|
if (result.IsInvalid)
|
||
|
|
{
|
||
|
|
roleRenders++;
|
||
|
|
Toast.AddResponseError(result);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var changed = result.Data as ViewAdminRoot;
|
||
|
|
if (changed is not null)
|
||
|
|
roots = [.. roots.Select(r => r.Id == root.Id ? changed : r)];
|
||
|
|
Toast.AddResponseSuccess(Localizer["{0} is now {1}.", root.UserName, RoleName(changed?.Role ?? role)]);
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task Ban(ViewAdminRoot root)
|
||
|
|
{
|
||
|
|
if (!await Confirm.Ask(Localizer["Ban {0}?", root.UserName],
|
||
|
|
Localizer["Its sessions end and its personas are held back as suspended ones, without a word written on any of them. It can be lifted."],
|
||
|
|
Localizer["Ban"], true))
|
||
|
|
return;
|
||
|
|
|
||
|
|
await Run(root, ModerationService.Ban, Localizer["{0} is banned.", root.UserName]);
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task Unban(ViewAdminRoot root)
|
||
|
|
{
|
||
|
|
if (!await Confirm.Ask(Localizer["Lift {0}'s ban?", root.UserName], Localizer["Its personas are given back as they were."], Localizer["Unban"], false))
|
||
|
|
return;
|
||
|
|
|
||
|
|
await Run(root, ModerationService.Unban, Localizer["{0} is no longer banned.", root.UserName]);
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task ResetTotp(ViewAdminRoot root)
|
||
|
|
{
|
||
|
|
if (!await Confirm.Ask(Localizer["Turn off {0}'s two-factor sign-in?", root.UserName],
|
||
|
|
Localizer["For a login that lost its authenticator app and its recovery codes: its password and its sessions stay."],
|
||
|
|
Localizer["Reset two-factor"], true))
|
||
|
|
return;
|
||
|
|
|
||
|
|
await Run(root, ModerationService.ResetTotp, Localizer["{0} signs in with its password alone now.", root.UserName]);
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task DeleteRoot(ViewAdminRoot root)
|
||
|
|
{
|
||
|
|
if (!await Confirm.Ask(Localizer["Delete {0}?", root.UserName],
|
||
|
|
Localizer["Every persona of it goes with it, and everything public they had, here and on the servers that hold it. Its names stay taken. It cannot be undone."],
|
||
|
|
Localizer["Delete for good"], true))
|
||
|
|
return;
|
||
|
|
|
||
|
|
busy.Add(root.Id);
|
||
|
|
var result = await ModerationService.DeleteRoot(root.Id);
|
||
|
|
busy.Remove(root.Id);
|
||
|
|
if (result.IsInvalid)
|
||
|
|
{
|
||
|
|
Toast.AddResponseError(result);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
roots = [.. roots.Where(r => r.Id != root.Id)];
|
||
|
|
Toast.AddResponseSuccess(Localizer["{0} is deleted.", root.UserName]);
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task Run(ViewAdminRoot root, Func<string, CancellationToken, Task<WebResult>> change, string done)
|
||
|
|
{
|
||
|
|
busy.Add(root.Id);
|
||
|
|
var result = await change(root.Id, default);
|
||
|
|
busy.Remove(root.Id);
|
||
|
|
if (result.IsInvalid)
|
||
|
|
{
|
||
|
|
Toast.AddResponseError(result);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await Refresh(root);
|
||
|
|
Toast.AddResponseSuccess(done);
|
||
|
|
}
|
||
|
|
|
||
|
|
string Named => actingGroup is { } group ? "@" + group.UserName
|
||
|
|
: actingPersona?.DisplayName is { Length: > 0 } displayName ? displayName
|
||
|
|
: "@" + actingPersona?.Handle;
|
||
|
|
|
||
|
|
string Question => act switch
|
||
|
|
{
|
||
|
|
ModerationActions.Silence => Localizer["Silence {0}?", Named],
|
||
|
|
ModerationActions.Unsilence => Localizer["Unsilence {0}?", Named],
|
||
|
|
ModerationActions.Suspend => Localizer["Suspend {0}?", Named],
|
||
|
|
ModerationActions.Unsuspend => Localizer["Unsuspend {0}?", Named],
|
||
|
|
PersonaDelete => Localizer["Delete {0}?", Named],
|
||
|
|
_ => Localizer["Delete the group {0}?", Named]
|
||
|
|
};
|
||
|
|
|
||
|
|
string ActText => act switch
|
||
|
|
{
|
||
|
|
ModerationActions.Silence => Localizer["Its posts stay out of public timelines, hashtags, trends and search; its followers still read them."],
|
||
|
|
ModerationActions.Suspend => 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.Unsilence => Localizer["Its posts are listed again."],
|
||
|
|
ModerationActions.Unsuspend => Localizer["It is given back as it was, but the sessions a suspension ended."],
|
||
|
|
PersonaDelete when actingPersona?.Groups.Count > 0 => Localizer["Everything public it had goes, here and on the servers that hold it, and the {0} groups it owns with it. Its name stays taken. It cannot be undone.",
|
||
|
|
actingPersona.Groups.Count],
|
||
|
|
PersonaDelete => Localizer["Everything public it had goes, here and on the servers that hold it, with the groups it owns. Its name stays taken. It cannot be undone."],
|
||
|
|
_ => Localizer["Its members and followers are told, and its name stays taken. It cannot be undone."]
|
||
|
|
};
|
||
|
|
|
||
|
|
string ConfirmText => act switch
|
||
|
|
{
|
||
|
|
ModerationActions.Silence => Localizer["Silence"],
|
||
|
|
ModerationActions.Unsilence => Localizer["Unsilence"],
|
||
|
|
ModerationActions.Suspend => Localizer["Suspend"],
|
||
|
|
ModerationActions.Unsuspend => Localizer["Unsuspend"],
|
||
|
|
_ => Localizer["Delete for good"]
|
||
|
|
};
|
||
|
|
|
||
|
|
void Ask(ViewAdminRoot root, ViewAdminPersona persona, ViewAdminGroup group, string what) =>
|
||
|
|
(actingRoot, actingPersona, actingGroup, act) = (root, persona, group, what);
|
||
|
|
|
||
|
|
void CloseAct() => (actingRoot, actingPersona, actingGroup, act) = (null, null, null, null);
|
||
|
|
|
||
|
|
async Task<WebResult> Act(ActDialog.Answer answer)
|
||
|
|
{
|
||
|
|
var (root, persona, group, what) = (actingRoot, actingPersona, actingGroup, act);
|
||
|
|
var result = what switch
|
||
|
|
{
|
||
|
|
PersonaDelete => await ModerationService.DeletePersona(persona.Id, new AdminPasswordForm { Password = answer.Password, Note = answer.Note }),
|
||
|
|
GroupDelete => await ModerationService.DeleteGroup(group.Id, new AdminPasswordForm { Password = answer.Password, Note = answer.Note }),
|
||
|
|
_ => await ModerationService.Moderate(new ModerateAccountForm
|
||
|
|
{
|
||
|
|
AccountId = persona.Id,
|
||
|
|
Action = what,
|
||
|
|
Note = answer.Note,
|
||
|
|
Warning = what is ModerationActions.Silence or ModerationActions.Suspend ? answer.Warning : null
|
||
|
|
})
|
||
|
|
};
|
||
|
|
if (result.IsInvalid)
|
||
|
|
return result;
|
||
|
|
|
||
|
|
await Refresh(root);
|
||
|
|
// one of the administrator's own personas: the session follows it
|
||
|
|
if (root.IsYou && what == PersonaDelete)
|
||
|
|
await CascadingState.PersonaDeleted(persona.Id);
|
||
|
|
else if (root.IsYou)
|
||
|
|
await CascadingState.ReloadPersonas();
|
||
|
|
Toast.AddResponseSuccess(what switch
|
||
|
|
{
|
||
|
|
ModerationActions.Silence => Localizer["{0} is silenced.", Named],
|
||
|
|
ModerationActions.Unsilence => Localizer["{0} is no longer silenced.", Named],
|
||
|
|
ModerationActions.Suspend => Localizer["{0} is suspended.", Named],
|
||
|
|
ModerationActions.Unsuspend => Localizer["{0} is no longer suspended.", Named],
|
||
|
|
_ => Localizer["{0} is deleted.", Named]
|
||
|
|
});
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
}
|