Administration's Users & personas (administrators): logins searched and opened into their personas and groups; roles, bans, two-factor reset and deletion of a login; silence, suspension and deletion of a persona; a group deleted. Accounts (moderators): local personas and remote accounts searched or listed as moderated, silenced and suspended with a staff note and a warning. Reports resolve with an action. ActDialog asks for the note, the warning or the password and is the act's confirmation. A login deletes its own persona and the groups it owns with its password. Suspended personas can't be chosen; one refused a token mid-session moves the session to another, or to none, once. Warnings show in notifications, limited accounts on their profile, and moderators remove a post from its card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
133 lines
5.7 KiB
Plaintext
133 lines
5.7 KiB
Plaintext
@inherits LocalizableComponentBase
|
|
|
|
@* The root's personas (/clientapi/avatar/private): each one used in turn, a new one made, and one deleted with the
|
|
login's password, asked first (everything public it had goes, with the groups it owns). One the server's staff
|
|
suspended or silenced says so; a suspended one can't be used until the suspension is lifted, but may be deleted.
|
|
Personas are never linked to one another: no server, PrivaPub's included, shows that they share a root. *@
|
|
<div class="flex flex-col gap-4 w-full" data-personas>
|
|
<div class="flex flex-col gap-2">
|
|
@if (CascadingState.Personas.Count == 0)
|
|
{
|
|
<p class="text-sm" data-personas-none>@Localizer["This login has no persona to act as yet: make one below."]</p>
|
|
}
|
|
@foreach (var persona in CascadingState.Personas)
|
|
{
|
|
<div class="flex flex-wrap items-center gap-2 pointer-coarse:gap-2 min-w-0" data-persona="@persona.UserName" data-persona-state="@StateOf(persona)">
|
|
<img class="size-8 rounded-full object-cover" src="@(persona.PictureURL ?? "imgs/icon-192.png")" alt="" />
|
|
<span class="flex-1 min-w-0 truncate"><b>@(persona.Name ?? persona.UserName)</b> <span class="text-xs opacity-70">@persona.Handle</span></span>
|
|
@if (persona.AccountState is ViewAvatarState.Suspended)
|
|
{
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Error" title="@Localizer["Suspended by this server's staff: it can't act until that is lifted."]">
|
|
@Localizer["Suspended"]
|
|
</DBadge>
|
|
}
|
|
else if (persona.AccountState is ViewAvatarState.Silenced)
|
|
{
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Warning" title="@Localizer["Silenced by this server's staff: its posts stay out of public timelines, trends and search."]">
|
|
@Localizer["Silenced"]
|
|
</DBadge>
|
|
}
|
|
@if (persona.Id == CascadingState.PersonaId)
|
|
{
|
|
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Primary">@Localizer["in use"]</DBadge>
|
|
}
|
|
else
|
|
{
|
|
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || switching is not null || !CascadingState.CanUse(persona))" OnClick="() => Use(persona)">
|
|
@Localizer["Use"]
|
|
</DButton>
|
|
}
|
|
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || switching is not null)" OnClick="() => deleting = persona"
|
|
title="@Localizer["Delete this persona"]" aria-label="@Localizer["Delete this persona"]" data-persona-delete-own>
|
|
<i class="ion-md-trash text-error" aria-hidden="true"></i><span class="hidden sm:inline">@Localizer["Delete"]</span>
|
|
</DButton>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (deleting is not null)
|
|
{
|
|
<ActDialog Question="@Localizer["Delete {0}?", deleting.Name ?? "@" + deleting.UserName]"
|
|
Text="@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."]"
|
|
ConfirmText="@Localizer["Delete for good"]" Danger="true" Act="persona-delete" AskPassword="true" OnConfirm="Delete"
|
|
OnClosed="() => deleting = null" />
|
|
}
|
|
|
|
<EditForm Model="form" OnValidSubmit="Create" class="flex flex-col gap-2 p-3 rounded-xl neo-inset neo-2xs" data-persona-new>
|
|
<DataAnnotationsValidator />
|
|
<b class="text-sm">@Localizer["A new persona"]</b>
|
|
<div class="flex flex-wrap gap-2">
|
|
<DInput TValue="string" class="flex-1 min-w-40" @bind-Value="form.UserName" Immediate="true" autocomplete="off"
|
|
maxlength="@PrivaPub.ClientModels.Constants.MaxAvatarNameLength" placeholder="@Localizer["user name (a-z, 0-9, _)"]" data-persona-username>
|
|
<Prefix>@@</Prefix>
|
|
</DInput>
|
|
<DInput TValue="string" class="flex-1 min-w-40" @bind-Value="form.Name" Immediate="true" autocomplete="off"
|
|
maxlength="@PrivaPub.ClientModels.Constants.MaxAvatarNameLength" placeholder="@Localizer["Display name"]" data-persona-name />
|
|
</div>
|
|
<DTextarea @bind-Value="form.Biography" Immediate="true" rows="3" autocomplete="off" maxlength="@ProfileForm.NoteLength"
|
|
placeholder="@Localizer["Biography"]" data-persona-biography />
|
|
<ErrorViewer />
|
|
<DButton Type="submit" Size="DSize.Sm" Color="DColor.Primary" class="self-end" Disabled="@(IsDefaultDisabled || isSaving)" data-persona-create>
|
|
<i class="ion-md-person-add"></i><span>@Localizer["Make the persona"]</span>
|
|
</DButton>
|
|
</EditForm>
|
|
</div>
|
|
|
|
@code {
|
|
[Inject] AuthService AuthService { get; set; }
|
|
|
|
InsertAvatarForm form = new();
|
|
bool isSaving;
|
|
string switching;
|
|
// the persona whose deletion the dialog asks about
|
|
ViewAvatar deleting;
|
|
|
|
static string StateOf(ViewAvatar persona) => persona.AccountState switch
|
|
{
|
|
ViewAvatarState.Suspended => "suspended",
|
|
ViewAvatarState.Silenced => "silenced",
|
|
_ => "normal"
|
|
};
|
|
|
|
protected override void OnInitialized() => IsLoading = false;
|
|
|
|
async Task Use(ViewAvatar persona)
|
|
{
|
|
switching = persona.Id;
|
|
var result = await CascadingState.SwitchPersona(persona.Id);
|
|
switching = null;
|
|
if (result.IsInvalid)
|
|
Toast.AddResponseError(result);
|
|
}
|
|
|
|
async Task<WebResult> Delete(ActDialog.Answer answer)
|
|
{
|
|
var persona = deleting;
|
|
var result = await AuthService.DeletePersona(persona.Id, answer.Password);
|
|
if (result.IsInvalid)
|
|
return result;
|
|
|
|
// the session drops it, and acts as another persona when it was the current one (none left: make one below)
|
|
await CascadingState.PersonaDeleted(persona.Id);
|
|
Toast.AddResponseSuccess(Localizer["{0} is deleted.", persona.Name ?? "@" + persona.UserName]);
|
|
return result;
|
|
}
|
|
|
|
async Task Create()
|
|
{
|
|
isSaving = true;
|
|
form.UserName = form.UserName?.Trim().TrimStart('@').ToLowerInvariant();
|
|
var result = await AuthService.CreatePersona(form);
|
|
isSaving = false;
|
|
if (result.IsInvalid)
|
|
{
|
|
Toast.AddResponseError(result);
|
|
return;
|
|
}
|
|
|
|
form = new();
|
|
await CascadingState.ReloadPersonas();
|
|
Toast.AddResponseSuccess(Localizer["Persona made."]);
|
|
}
|
|
}
|