@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. *@
@Localizer["Add the relay"]

@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."]

@if (IsLoading) {

@Localizer["loading"]

} else if (relays.Count == 0) {

@Localizer["The server subscribes to no relay."]

} @foreach (var relay in relays) {
@relay.Address @switch (relay.State) { case "accepted": @Localizer["Subscribed"] break; case "pending": @Localizer["Waiting for the relay"] break; case "rejected": @Localizer["Refused by the relay"] break; default: @Localizer["Not asked yet"] break; } @if (relay.Source == "configuration") { @Localizer["Configured"] } else { @Localizer["Remove"] }
@if (relay.ActorURI is { Length: > 0 } actor && actor != relay.Address) {

@Localizer["Its actor: {0}", actor]

}

@if (relay.RequestedAt is { } requested) { @Localizer["Asked {0} ago", requested.GetPassedTime(Localizer)] } @if (relay.AnsweredAt is { } answered) { ยท @Localizer["answered {0} ago", answered.GetPassedTime(Localizer)] }

@if (relay.Source == "configuration") {

@Localizer["Named by the configuration (Federation:Relays): removed only there."]

}
}
@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 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)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)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(); } }