2026-10-05 06:20:18 +02:00
@inherits LocalizableComponentBase
2026-10-17 05:41:10 +02:00
@* 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. *@
2026-10-05 06:20:18 +02:00
<div class="flex flex-col gap-4 w-full" data-personas>
<div class="flex flex-col gap-2">
2026-10-17 05:41:10 +02:00
@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>
}
2026-10-05 06:20:18 +02:00
@foreach (var persona in CascadingState.Personas)
{
2026-10-17 05:41:10 +02:00
<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)">
2026-10-05 06:20:18 +02:00
<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>
2026-10-17 05:41:10 +02:00
@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>
}
2026-10-05 06:20:18 +02:00
@if (persona.Id == CascadingState.PersonaId)
{
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Primary">@Localizer["in use"]</DBadge>
}
else
{
2026-10-17 05:41:10 +02:00
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || switching is not null || !CascadingState.CanUse(persona))" OnClick="() => Use(persona)">
@Localizer["Use"]
</DButton>
2026-10-05 06:20:18 +02:00
}
2026-10-17 05:41:10 +02:00
<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>
2026-10-05 06:20:18 +02:00
</div>
}
</div>
2026-10-17 05:41:10 +02:00
@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" />
}
2026-10-05 06:20:18 +02:00
<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;
2026-10-17 05:41:10 +02:00
// 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"
};
2026-10-05 06:20:18 +02:00
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);
}
2026-10-17 05:41:10 +02:00
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;
}
2026-10-05 06:20:18 +02:00
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."]);
}
}