Files
decePubClient/Components/Admin/RelaysPanel.razor
T
thepraandClaude Opus 5.5 4c88980374
Build / Build (push) Successful in 1m5s
R5 the server's own settings in decePub: instance, rules, announcements, relays, media limits, the staff's log, About
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
2026-10-17 04:32:57 +02:00

192 lines
6.4 KiB
Plaintext

@inherits LocalizableComponentBase
@* The relays the server's actor subscribes to (/clientapi/admin/relays): the configuration's (Federation:Relays, removed
only there) and the ones added here, each with its subscription's state. PrivaPub answers an addition or a removal at
once (202) and subscribes or unsubscribes in the background, since reaching a relay takes a fetch, so the panel reads
the list again a few seconds later. *@
<div class="flex flex-col gap-3 w-full" data-relays>
<div class="flex flex-wrap items-center gap-2 pointer-coarse:gap-2">
<DInput TValue="string" class="flex-1 min-w-48" Type="url" @bind-Value="address" Immediate="true" autocomplete="off" maxlength="2048"
placeholder="@Localizer["https://relay.example/actor"]" aria-label="@Localizer["The relay's address"]" data-relay-address>
<Prefix><i class="ion-md-radio" aria-hidden="true"></i></Prefix>
</DInput>
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="IsDefaultDisabled || isSaving || string.IsNullOrWhiteSpace(address)" OnClick="Add" data-relay-add>
<i class="ion-md-add" aria-hidden="true"></i><span>@Localizer["Add the relay"]</span>
</DButton>
</div>
<p class="text-xs opacity-70">@Localizer["The https address of the relay's actor, or of its inbox. Public posts of this server go to it, and its posts come here."]</p>
@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 (relays.Count == 0)
{
<p class="text-sm opacity-70" data-relays-empty>@Localizer["The server subscribes to no relay."]</p>
}
@foreach (var relay in relays)
{
<div class="flex flex-col gap-1 p-2 rounded-lg neo-raised neo-2xs" data-relay="@relay.Address" data-relay-state="@relay.State"
data-relay-source="@relay.Source">
<div class="flex flex-wrap items-center gap-2 pointer-coarse:gap-2">
<b class="flex-1 min-w-0 break-all text-sm">@relay.Address</b>
@switch (relay.State)
{
case "accepted":
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Success">@Localizer["Subscribed"]</DBadge>
break;
case "pending":
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Warning">@Localizer["Waiting for the relay"]</DBadge>
break;
case "rejected":
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Error">@Localizer["Refused by the relay"]</DBadge>
break;
default:
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft">@Localizer["Not asked yet"]</DBadge>
break;
}
@if (relay.Source == "configuration")
{
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Outline" title="@Localizer["Named by the configuration (Federation:Relays): removed only there."]">
@Localizer["Configured"]
</DBadge>
}
else
{
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled || isSaving" OnClick="() => Remove(relay)" data-relay-remove>
<i class="ion-md-trash text-error" aria-hidden="true"></i><span>@Localizer["Remove"]</span>
</DButton>
}
</div>
@if (relay.ActorURI is { Length: > 0 } actor && actor != relay.Address)
{
<p class="text-xs opacity-70 break-all">@Localizer["Its actor: {0}", actor]</p>
}
<p class="text-xs opacity-70">
@if (relay.RequestedAt is { } requested)
{
<span title="@requested.ToLocalTime().ToString(SUtility.dateTimeFormatExt)">@Localizer["Asked {0} ago", requested.GetPassedTime(Localizer)]</span>
}
@if (relay.AnsweredAt is { } answered)
{
<span title="@answered.ToLocalTime().ToString(SUtility.dateTimeFormatExt)"> · @Localizer["answered {0} ago", answered.GetPassedTime(Localizer)]</span>
}
</p>
@if (relay.Source == "configuration")
{
<p class="text-xs opacity-70">@Localizer["Named by the configuration (Federation:Relays): removed only there."]</p>
}
</div>
}
</div>
@code {
// when the list is read again after a change: subscribing to a relay is a fetch, then its answer
static readonly TimeSpan[] Rereads = [TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(15)];
[Inject] InstanceAdminService InstanceAdminService { get; set; }
List<ViewRelay> relays = [];
string address;
bool isSaving;
CancellationTokenSource rereading;
protected override async Task OnInitializedAsync()
{
if (CascadingState.IsOffline)
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
await LoadData();
IsLoading = false;
}
async Task LoadData() => relays = await InstanceAdminService.Relays();
async Task Add()
{
isSaving = true;
var result = await InstanceAdminService.AddRelay(address);
isSaving = false;
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
relays = (List<ViewRelay>)result.Data;
address = null;
Toast.AddResponseSuccess(Localizer["The relay is added: the server subscribes to it now."]);
await ReadAgainSoon();
}
async Task Remove(ViewRelay relay)
{
isSaving = true;
var result = await InstanceAdminService.RemoveRelay(relay.Address);
isSaving = false;
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
relays = (List<ViewRelay>)result.Data;
Toast.AddResponseSuccess(Localizer["The relay is removed: the server unsubscribes from it now."]);
await ReadAgainSoon();
}
// the background subscription answers later: the list is read again, the last change's timer replacing the one before
async Task ReadAgainSoon()
{
if (rereading is not null)
{
await rereading.CancelAsync();
rereading.Dispose();
}
rereading = new();
_ = ReadAgain(rereading.Token);
}
async Task ReadAgain(CancellationToken cancellationToken)
{
foreach (var pause in Rereads)
{
try
{
await Task.Delay(pause, cancellationToken);
}
catch (OperationCanceledException)
{
return;
}
var read = await InstanceAdminService.Relays(cancellationToken);
if (cancellationToken.IsCancellationRequested)
return;
// an empty answer to a list that had relays is a failed read: a change here showed its own list at once
if (read.Count == 0 && relays.Count > 0)
continue;
relays = read;
await InvokeAsync(StateHasChanged);
}
}
public override async ValueTask DisposeAsync()
{
if (rereading is not null)
{
await rereading.CancelAsync();
rereading.Dispose();
}
await base.DisposeAsync();
}
}