Build / Build (push) Successful in 1m5s
Administration's Pleroma placeholders give way to real sections: Instance (title, descriptions, contact, thumbnail, privacy policy, terms, the weekly activity), Rules, Announcements, Relays, Media limits and the Moderation log, next to Reports, blocked servers (now published or obfuscated), statistics and backups; moderators see Reports and the log. Every save sends the whole settings form built from the latest read, since PrivaPub replaces what it carries. /about shows the server's documents to anyone. The report dialog picks the rules a violation breaks, and reports show them. Announcements show above the feed tabs, dismissed and reacted to, live from the user stream. Login names the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
245 lines
9.3 KiB
Plaintext
245 lines
9.3 KiB
Plaintext
@inherits LocalizableComponentBase
|
|
|
|
@* On a phone the menu opens from the hamburger over the page, and closes after a choice (any navigation, a persona
|
|
switch), on a tap outside it or on Escape; from md it is the sidebar, which scrolls on its own when the window is
|
|
short (a phone held sideways). *@
|
|
<nav class="flex justify-between items-center md:items-start pt-3 px-3 md:p-0 md:h-full md:overflow-y-auto md:overscroll-contain"
|
|
@onkeydown="OnKeyDown">
|
|
|
|
@if (isMenuOpen)
|
|
{
|
|
<div class="md:hidden fixed inset-0 z-40 bg-black/30" aria-hidden="true" @onclick="CloseMenu"></div>
|
|
}
|
|
<div class="relative md:w-52">
|
|
<DButton Size="DSize.Sm" class="md:hidden" OnClick="ToggleNavMenu" aria-label="@Localizer["Menu"]" title="@Localizer["Menu"]"
|
|
aria-expanded="@(isMenuOpen ? "true" : "false")" aria-controls="nav-panel" data-nav-toggle>
|
|
<i class="ion-md-menu text-lg" aria-hidden="true"></i>
|
|
</DButton>
|
|
<div id="nav-panel" data-nav-panel
|
|
class="@(isMenuOpen ? "block" : "hidden") md:block absolute bg-base-100 md:relative top-full md:top-auto mt-2 w-[min(18rem,calc(100vw-1.5rem))] md:w-full max-h-[calc(100dvh-5rem)] md:max-h-none overflow-y-auto overscroll-contain rounded-lg neo-raised neo-sm md:shadow-none! z-50">
|
|
<div class="flex flex-col gap-3 p-3 md:p-4">
|
|
|
|
@if (CascadingState.IsSignedIn)
|
|
{
|
|
<DDropdown @bind-IsOpen="isPersonaMenuOpen" CloseOnContentClick="true" class="w-full rounded-xl bg-cover bg-no-repeat bg-right"
|
|
TriggerClass="flex w-full gap-3 p-3 md:p-4 neo-raised neo-xs rounded-xl text-left cursor-pointer"
|
|
ContentClass="mt-2 flex flex-col gap-2 p-2 rounded-box w-full" title="@Localizer["Switch persona"]"
|
|
style="@PersonaBackground">
|
|
<Trigger>
|
|
<img alt="" class="h-8 w-8 md:h-12 md:w-12 flex-none object-cover rounded-full neo-raised neo-2xs"
|
|
src="@(CascadingState.Persona?.Avatar ?? "imgs/icon-192.png")" />
|
|
<span class="flex flex-col w-full min-w-0 gap-1">
|
|
<b class="truncate" title="@CascadingState.Persona?.Name">@(CascadingState.Persona?.Name ?? Localizer["No persona"])</b>
|
|
<span class="truncate text-xs opacity-70">@(CascadingState.Persona is { } persona ? "@" + persona.Acct : null)</span>
|
|
</span>
|
|
<i class="ion-md-swap self-center opacity-60"></i>
|
|
</Trigger>
|
|
<ChildContent>
|
|
@foreach (var persona in CascadingState.Personas)
|
|
{
|
|
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Active="persona.Id == CascadingState.PersonaId"
|
|
Disabled="IsDefaultDisabled || persona.Id == CascadingState.PersonaId" OnClick="() => SwitchPersona(persona.Id)">
|
|
<img alt="" class="h-5 w-5 object-cover rounded-full" src="@(persona.PictureURL ?? "imgs/icon-192.png")" />
|
|
<span class="truncate">@(persona.Name ?? persona.UserName)</span>
|
|
</DButton>
|
|
}
|
|
</ChildContent>
|
|
</DDropdown>
|
|
}
|
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="/" Match="NavLinkMatch.All">
|
|
<i class="ion-md-home"></i>
|
|
<span>@Localizer["Home"]</span>
|
|
</NavLink>
|
|
@if (CascadingState.IsSignedIn)
|
|
{
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="search">
|
|
<i class="ion-md-search"></i>
|
|
<span>@Localizer["Search"]</span>
|
|
</NavLink>
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="explore">
|
|
<i class="ion-md-compass"></i>
|
|
<span>@Localizer["Explore"]</span>
|
|
</NavLink>
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="lists">
|
|
<i class="ion-md-list"></i>
|
|
<span>@Localizer["Lists"]</span>
|
|
</NavLink>
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="groups">
|
|
<i class="ion-md-people"></i>
|
|
<span>@Localizer["Groups"]</span>
|
|
</NavLink>
|
|
@if (CascadingState.PersonaId is { } personaId)
|
|
{
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="@($"account/{Uri.EscapeDataString(personaId)}")">
|
|
<i class="ion-md-person"></i>
|
|
<span>@Localizer["My profile"]</span>
|
|
</NavLink>
|
|
}
|
|
}
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="settings">
|
|
<i class="ion-md-settings"></i>
|
|
<span>@Localizer["Settings"]</span>
|
|
</NavLink>
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="about" data-nav-about>
|
|
<i class="ion-md-information-circle"></i>
|
|
<span>@Localizer["About this server"]</span>
|
|
</NavLink>
|
|
<AuthorizeView Policy="@Policies.IsModerator">
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="administration">
|
|
<i class="ion-md-switch text-error"></i>
|
|
<span>@Localizer["Administration"]</span>
|
|
</NavLink>
|
|
</AuthorizeView>
|
|
@if (CascadingState.IsSignedIn)
|
|
{
|
|
<DButton class="justify-start" OnClick="Logout" data-logout>
|
|
<i class="ion-md-log-out text-error"></i>
|
|
<span>@Localizer["Logout"]</span>
|
|
</DButton>
|
|
}
|
|
else
|
|
{
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="login">
|
|
<i class="ion-md-log-in text-success"></i>
|
|
<span>@Localizer["Login"]</span>
|
|
</NavLink>
|
|
}
|
|
|
|
<div class="flex items-center gap-3">
|
|
<DButton Size="DSize.Sm" Shape="DButtonShape.Circle" class="flex-none" OnClick="ResetToOriginalColour"
|
|
title="@Localizer["Reset"]">
|
|
<i class="ion-md-refresh"></i>
|
|
</DButton>
|
|
<DRange TValue="short" class="neo-hue-track w-full" Size="DSize.Xs" Min="0" Max="359" Step="1"
|
|
Value="ThemeIndexColour" ValueChanged="UpdateThemeColour" ValueExpression="() => ThemeIndexColour" />
|
|
</div>
|
|
|
|
<div class="flex justify-end">
|
|
<DToggle Value="IsDark" ValueChanged="UpdateThemeDarkMode" ValueExpression="() => IsDark" class="touch-target"
|
|
aria-label="@Localizer["Dark mode"]" title="@Localizer["Dark mode"]">
|
|
<UncheckedIcon>
|
|
<i class="ion-md-sunny"></i>
|
|
</UncheckedIcon>
|
|
<CheckedIcon>
|
|
<i class="ion-md-moon"></i>
|
|
</CheckedIcon>
|
|
</DToggle>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<a class="md:hidden grid place-items-center h-11 w-11" href="" aria-label="@Localizer["Home"]" title="@Localizer["Home"]">
|
|
<img alt="" class="h-[30px]" src="imgs/icon-512.png">
|
|
</a>
|
|
|
|
<a class="md:hidden grid place-items-center h-11 w-11" href="settings" aria-label="@Localizer["Settings"]" title="@Localizer["Settings"]">
|
|
<img alt="@CascadingState.Persona?.Name" class="object-cover w-[30px] h-[30px] rounded-full neo-raised neo-2xs"
|
|
src="@(CascadingState.Persona?.Avatar ?? "imgs/icon-192.png")">
|
|
</a>
|
|
|
|
</nav>
|
|
|
|
@code {
|
|
[Inject] ConfirmService Confirm { get; set; }
|
|
|
|
[Inject] OverlayService Overlays { get; set; }
|
|
|
|
bool isMenuOpen;
|
|
IDisposable overlay;
|
|
|
|
// open, the Back button closes it
|
|
void SetMenu(bool open)
|
|
{
|
|
isMenuOpen = open;
|
|
if (open && overlay is null)
|
|
overlay = Overlays.Open(() => InvokeAsync(() =>
|
|
{
|
|
SetMenu(false);
|
|
StateHasChanged();
|
|
}));
|
|
else if (!open && overlay is not null)
|
|
{
|
|
overlay.Dispose();
|
|
overlay = null;
|
|
}
|
|
}
|
|
bool isPersonaMenuOpen;
|
|
|
|
string PersonaBackground => CascadingState.Persona?.Header is { Length: > 0 } header && !header.Contains("/missing-")
|
|
? $"background-image:linear-gradient(to left, var(--color-base-100) 55%, transparent), url('{header}');"
|
|
: null;
|
|
|
|
// The toggle and the slider work on the variant that is showing; with system theming that is the OS's choice.
|
|
bool IsDark => CascadingState.Status.IsDarkTheme();
|
|
short ThemeIndexColour => IsDark ? CascadingState.PublicCacheData.PageSettings.DarkThemeIndexColour : CascadingState.PublicCacheData.PageSettings.LightThemeIndexColour;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
IsLoading = false;
|
|
Navigation.LocationChanged += OnLocationChanged;
|
|
}
|
|
|
|
void OnLocationChanged(object sender, LocationChangedEventArgs e)
|
|
{
|
|
if (!isMenuOpen)
|
|
return;
|
|
SetMenu(false);
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
void CloseMenu() => SetMenu(false);
|
|
|
|
void OnKeyDown(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Escape")
|
|
SetMenu(false);
|
|
}
|
|
|
|
public override async ValueTask DisposeAsync()
|
|
{
|
|
Navigation.LocationChanged -= OnLocationChanged;
|
|
overlay?.Dispose();
|
|
await base.DisposeAsync();
|
|
}
|
|
|
|
async Task SwitchPersona(string personaId)
|
|
{
|
|
SetMenu(false);
|
|
var result = await CascadingState.SwitchPersona(personaId);
|
|
if (result.IsInvalid)
|
|
Toast.AddResponseError(result);
|
|
}
|
|
|
|
private void ToggleNavMenu() => SetMenu(!isMenuOpen);
|
|
|
|
async Task Logout()
|
|
{
|
|
SetMenu(false);
|
|
if (!await Confirm.Ask(Localizer["Log out?"], Localizer["Every persona of this login is signed out on this device."], Localizer["Logout"], true))
|
|
return;
|
|
Navigation.NavigateTo("logout");
|
|
}
|
|
|
|
protected async Task ResetToOriginalColour()
|
|
{
|
|
var isDark = IsDark;
|
|
await CascadingState.UpdatePageSettings(settings => settings.SetHue(isDark,
|
|
isDark ? PageSettings.DefaultDarkThemeIndexColour : PageSettings.DefaultLightThemeIndexColour));
|
|
}
|
|
|
|
// Picking a hue also leaves the grey themes, which otherwise ignore it.
|
|
protected async Task UpdateThemeColour(short indexColour)
|
|
{
|
|
var isDark = IsDark;
|
|
await CascadingState.UpdatePageSettings(settings => settings.SetHue(isDark, indexColour));
|
|
}
|
|
|
|
// Choosing light or dark here leaves system theming; Settings can turn it back on.
|
|
protected async Task UpdateThemeDarkMode(bool isDarkMode)
|
|
{
|
|
await CascadingState.UpdatePageSettings(settings => settings.SetDarkMode(isDarkMode));
|
|
}
|
|
} |