223 lines
8.2 KiB
Plaintext
223 lines
8.2 KiB
Plaintext
@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;
|
||
|
|
}
|