Files
decePubClient/Components/Settings/PersonasEditor.razor
T
thepraandClaude Opus 5.5 c572471b88 R6 moderation in decePub: users and personas, accounts, report actions, deletions, suspended personas
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
2026-10-17 05:41:10 +02:00

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