R5 the server's own settings in decePub: instance, rules, announcements, relays, media limits, the staff's log, About
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
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-17 04:32:57 +02:00
1 parent b8d7858acb
commit 4c88980374
33 files changed
+3743 -239

No files matched your search

+223
View File
@@ -0,0 +1,223 @@
@page "/about"
@inherits LocalizableComponentBase
<Title>@Localizer["About {0}", instance?.Title ?? "PrivaPub"]</Title>
@* What this server says of itself, to anyone, signed in or not (AboutService, the Mastodon instance API without
credentials): its name, short description and thumbnail, its long description, who answers for it, its rules, its
privacy policy and terms of service, and the domain blocks its administrator published (an obfuscated one partly
starred, as the server gives it). The documents are the server's sanitized HTML. *@
<section class="block relative w-full h-full neo-inset neo-xs rounded-xl">
<div class="flex flex-col items-center gap-4 p-4 md:p-5 w-full h-full absolute overflow-y-auto" data-about>
@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 (instance is null)
{
<div class="flex flex-col items-center gap-2 mt-6">
<p class="text-sm text-error">@Localizer["The server's description could not be read."]</p>
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled" OnClick="Retry">@Localizer["Retry"]</DButton>
</div>
}
else
{
<article class="flex flex-col w-full max-w-3xl rounded-xl neo-raised neo-sm overflow-hidden" data-about-server>
@if (instance.Thumbnail.Url is { Length: > 0 } thumbnail)
{
@* decorative: the server's name follows *@
<img src="@thumbnail" alt="" class="w-full aspect-[40/21] object-cover" />
}
<div class="flex flex-col gap-2 p-4 md:p-5">
<h1 class="text-2xl font-bold break-words" data-about-title>@instance.Title</h1>
@if (instance.Domain is { Length: > 0 } domain)
{
<p class="text-sm opacity-70 break-all">@domain</p>
}
@if (instance.Description is { Length: > 0 } summary)
{
<p class="text-base break-words" data-about-summary>@summary</p>
}
</div>
</article>
@if (description?.Content is { Length: > 0 } extended)
{
<article class="flex flex-col gap-2 w-full max-w-3xl p-4 md:p-5 rounded-xl neo-raised neo-xs" data-about-description>
<div class="post-content doc-content break-words">@((MarkupString)extended)</div>
@if (description.UpdatedAt is { } describedAt)
{
<p class="text-xs opacity-70">@Localizer["Last changed on {0}", describedAt.ToLocalTime().ToString(SUtility.dateTimeFormat)]</p>
}
</article>
}
@if (instance.Contact.Email is { Length: > 0 } || instance.Contact.Account is not null)
{
<article class="flex flex-col gap-3 w-full max-w-3xl p-4 md:p-5 rounded-xl neo-raised neo-xs" data-about-contact>
<h2 class="text-lg font-bold">@Localizer["Contact"]</h2>
@if (instance.Contact.Account is { } account)
{
<a class="flex items-center gap-3 p-2 rounded-lg neo-raised neo-2xs min-w-0" href="@AccountLink(account)" data-about-contact-account="@account.Id">
<img alt="" class="h-12 w-12 flex-none object-cover rounded-full" src="@(account.Avatar ?? "imgs/icon-192.png")" />
<span class="flex flex-col min-w-0">
<b class="truncate">@CustomEmojis.Text(account.Name, account.Emojis)</b>
<span class="text-xs opacity-70 truncate">@("@" + account.Acct)</span>
</span>
</a>
}
@if (instance.Contact.Email is { Length: > 0 } email)
{
<a class="link inline-flex items-center gap-2 break-all touch-target self-start" href="@($"mailto:{email}")" data-about-contact-email>
<i class="ion-md-mail" aria-hidden="true"></i>@email
</a>
}
</article>
}
<article class="flex flex-col gap-3 w-full max-w-3xl p-4 md:p-5 rounded-xl neo-raised neo-xs" data-about-rules>
<h2 class="text-lg font-bold">@Localizer["Rules"]</h2>
@if (instance.Rules.Count == 0)
{
<p class="text-sm opacity-70">@Localizer["The server has no rules."]</p>
}
else
{
<ol class="flex flex-col gap-2 list-decimal ps-6">
@foreach (var rule in instance.Rules)
{
<li class="break-words" data-about-rule="@rule.Id">
<span>@rule.Text</span>
@if (rule.Hint is { Length: > 0 } hint)
{
<p class="text-sm opacity-70">@hint</p>
}
</li>
}
</ol>
}
</article>
@if (privacy?.Content is { Length: > 0 } policy)
{
<article class="flex flex-col gap-2 w-full max-w-3xl p-4 md:p-5 rounded-xl neo-raised neo-xs" data-about-privacy>
<h2 class="text-lg font-bold">@Localizer["Privacy policy"]</h2>
<div class="post-content doc-content break-words">@((MarkupString)policy)</div>
@if (privacy.UpdatedAt is { } policyAt)
{
<p class="text-xs opacity-70">@Localizer["Last changed on {0}", policyAt.ToLocalTime().ToString(SUtility.dateTimeFormat)]</p>
}
</article>
}
@if (terms?.Content is { Length: > 0 } termsText)
{
<article class="flex flex-col gap-2 w-full max-w-3xl p-4 md:p-5 rounded-xl neo-raised neo-xs" data-about-terms>
<h2 class="text-lg font-bold">@Localizer["Terms of service"]</h2>
@if (TermsDate is { } effectiveOn)
{
<p class="text-xs opacity-70">
@(terms.Effective ? Localizer["In effect since {0}", effectiveOn] : Localizer["In effect from {0}", effectiveOn])
</p>
}
<div class="post-content doc-content break-words">@((MarkupString)termsText)</div>
</article>
}
@if (blocks.Count > 0)
{
<article class="flex flex-col gap-3 w-full max-w-3xl p-4 md:p-5 rounded-xl neo-raised neo-xs" data-about-blocks>
<h2 class="text-lg font-bold">@Localizer["Moderated servers"]</h2>
<p class="text-sm opacity-70">@Localizer["This server does not exchange with these, or keeps them out of its public feeds."]</p>
<ul class="flex flex-col gap-2">
@foreach (var block in blocks)
{
<li class="flex flex-col gap-1 p-2 rounded-lg neo-inset neo-2xs" data-about-block="@block.Domain">
<div class="flex flex-wrap items-center gap-2">
<b class="flex-1 min-w-0 break-all font-mono text-sm" title="@($"SHA-256 {block.Digest}")">@block.Domain</b>
@switch (block.Severity)
{
case "suspend":
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Error">@Localizer["Suspended"]</DBadge>
break;
case "silence":
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Warning">@Localizer["Silenced"]</DBadge>
break;
default:
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft">@Localizer["Limited"]</DBadge>
break;
}
</div>
@if (block.Comment is { Length: > 0 } reason)
{
<p class="text-xs break-words">@reason</p>
}
</li>
}
</ul>
</article>
}
}
</div>
</section>
@code {
[Inject] AboutService AboutService { get; set; }
InstanceV2 instance;
InstanceDocument description;
InstanceDocument privacy;
TermsOfService terms;
List<PublicDomainBlock> blocks = [];
// the terms' date as the reader writes dates
string TermsDate => DateTime.TryParseExact(terms?.EffectiveDate, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var on) ? on.ToString(SUtility.dateTimeFormat) : null;
protected override async Task OnInitializedAsync()
{
if (CascadingState.IsOffline)
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
await LoadData();
IsLoading = false;
}
async Task LoadData()
{
var reading = AboutService.Instance(fresh: true);
var describing = AboutService.ExtendedDescription();
var policing = AboutService.PrivacyPolicy();
var terming = AboutService.Terms();
var blocking = AboutService.DomainBlocks();
await Task.WhenAll(reading, describing, policing, terming, blocking);
instance = reading.Result;
description = describing.Result;
privacy = policing.Result;
terms = terming.Result;
blocks = blocking.Result;
}
async Task Retry()
{
IsLoading = true;
await LoadData();
IsLoading = false;
}
// signed in, the contact's profile in the app; signed out, its own page
string AccountLink(Account account) =>
CascadingState.IsSignedIn ? $"account/{Uri.EscapeDataString(account.Id)}" : account.Url;
}