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
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:
1 parent
b8d7858acb
commit
4c88980374
33 files changed
+3743
-239
No files matched your search
@@ -0,0 +1,177 @@
|
||||
@inherits LocalizableComponentBase
|
||||
|
||||
@* The server's rules (/clientapi/admin/settings): written, explained, put in order and removed here, then saved together.
|
||||
A rule keeps its id while it is listed, so the reports that name it keep their meaning; one removed is kept, greyed
|
||||
below, as old reports name it, and can come back. Saving starts from the settings PrivaPub has now
|
||||
(InstanceAdminService.SaveSettings), so what Instance or Media limits saved stays. *@
|
||||
<div class="flex flex-col gap-3 w-full" data-rules>
|
||||
@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 (settings is null)
|
||||
{
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<p class="text-sm text-error">@Localizer["The server's settings could not be read."]</p>
|
||||
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled" OnClick="Retry">@Localizer["Retry"]</DButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-xs opacity-70">@Localizer["Everyone reads them on the About page, and a report of a violation names the ones broken."]</p>
|
||||
@if (rows.Count == 0)
|
||||
{
|
||||
<p class="text-sm opacity-70" data-rules-empty>@Localizer["The server has no rules."]</p>
|
||||
}
|
||||
<ol class="flex flex-col gap-2">
|
||||
@foreach (var (row, position) in Numbered)
|
||||
{
|
||||
<li @key="row" class="flex flex-col gap-2 p-2 rounded-lg neo-raised neo-2xs" data-rule="@position" data-rule-id="@(row.Id ?? "new")">
|
||||
<div class="flex items-start gap-2 pointer-coarse:gap-2">
|
||||
<span class="mt-2 w-6 flex-none text-right text-sm font-semibold tabular-nums">@(position + 1).</span>
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<DTextarea TValue="string" class="w-full" @bind-Value="row.Text" Immediate="true" rows="2" autocomplete="off" maxlength="500"
|
||||
placeholder="@Localizer["The rule"]" aria-label="@Localizer["The rule"]" data-rule-text />
|
||||
<DInput TValue="string" class="w-full" Size="DSize.Sm" @bind-Value="row.Hint" Immediate="true" autocomplete="off" maxlength="1000"
|
||||
placeholder="@Localizer["What it means, if it needs saying (optional)"]" aria-label="@Localizer["Explanation"]" data-rule-hint />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 pointer-coarse:gap-2 flex-none">
|
||||
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled || isSaving || position == 0" OnClick="() => Move(position, -1)"
|
||||
aria-label="@Localizer["Move up"]" title="@Localizer["Move up"]" data-rule-up>
|
||||
<i class="ion-md-arrow-up" aria-hidden="true"></i>
|
||||
</DButton>
|
||||
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled || isSaving || position == rows.Count - 1" OnClick="() => Move(position, 1)"
|
||||
aria-label="@Localizer["Move down"]" title="@Localizer["Move down"]" data-rule-down>
|
||||
<i class="ion-md-arrow-down" aria-hidden="true"></i>
|
||||
</DButton>
|
||||
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled || isSaving" OnClick="() => Remove(row)"
|
||||
aria-label="@Localizer["Remove the rule"]" title="@Localizer["Remove the rule"]" data-rule-delete>
|
||||
<i class="ion-md-trash text-error" aria-hidden="true"></i>
|
||||
</DButton>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
<div class="flex flex-wrap items-center gap-2 pointer-coarse:gap-2">
|
||||
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled || isSaving || rows.Count >= ServerSettingsForm.MaxRules" OnClick="Add" data-rule-add>
|
||||
<i class="ion-md-add" aria-hidden="true"></i><span>@Localizer["Add a rule"]</span>
|
||||
</DButton>
|
||||
<span class="flex-1"></span>
|
||||
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled || isSaving" OnClick="Reset" data-rule-reset>@Localizer["Discard changes"]</DButton>
|
||||
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="IsDefaultDisabled || isSaving || rows.Any(r => string.IsNullOrWhiteSpace(r.Text))" OnClick="Save"
|
||||
data-rule-save>
|
||||
@if (isSaving)
|
||||
{
|
||||
<DLoading Size="DSize.Sm" LabelText="@Localizer["loading"]" />
|
||||
}
|
||||
<span>@Localizer["Save the rules"]</span>
|
||||
</DButton>
|
||||
</div>
|
||||
@if (rows.Count >= ServerSettingsForm.MaxRules)
|
||||
{
|
||||
<p class="text-xs opacity-70">@Localizer["A server has {0} rules at most.", ServerSettingsForm.MaxRules]</p>
|
||||
}
|
||||
|
||||
@if (Removed.Count > 0)
|
||||
{
|
||||
<div class="flex flex-col gap-2 mt-2" data-rules-deleted>
|
||||
<b class="text-sm">@Localizer["Removed rules, kept as old reports name them"]</b>
|
||||
@foreach (var rule in Removed)
|
||||
{
|
||||
<div class="flex flex-wrap items-center gap-2 p-2 rounded-lg neo-inset neo-2xs opacity-60" data-rule-deleted="@rule.Id">
|
||||
<span class="flex flex-col flex-1 min-w-0">
|
||||
<s class="text-sm break-words">@rule.Text</s>
|
||||
@if (rule.DeletedAt is { } deletedAt)
|
||||
{
|
||||
<span class="text-xs" title="@deletedAt.ToLocalTime().ToString(SUtility.dateTimeFormatExt)">@Localizer["Removed {0} ago", deletedAt.GetPassedTime(Localizer)]</span>
|
||||
}
|
||||
</span>
|
||||
<DButton Size="DSize.Xs" Disabled="IsDefaultDisabled || isSaving || rows.Count >= ServerSettingsForm.MaxRules" OnClick="() => Restore(rule)"
|
||||
data-rule-restore>
|
||||
<i class="ion-md-undo" aria-hidden="true"></i><span>@Localizer["Bring it back"]</span>
|
||||
</DButton>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] InstanceAdminService InstanceAdminService { get; set; }
|
||||
|
||||
ViewServerSettings settings;
|
||||
// the rules as being written, in their order: a listed rule keeps its id, a new one has none
|
||||
List<ServerRuleForm> rows = [];
|
||||
bool isSaving;
|
||||
|
||||
IEnumerable<(ServerRuleForm Row, int Position)> Numbered => rows.Select((row, position) => (row, position));
|
||||
|
||||
// the rules the server keeps deleted, unless they are listed again above
|
||||
List<ViewServerRule> Removed => [.. (settings?.Rules ?? []).Where(r => r.IsDeleted && rows.All(row => row.Id != r.Id))];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (CascadingState.IsOffline)
|
||||
{
|
||||
WhenOnline(LoadData);
|
||||
StartOnlinePolling();
|
||||
IsLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadData();
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
async Task LoadData()
|
||||
{
|
||||
settings = await InstanceAdminService.Settings();
|
||||
Reset();
|
||||
}
|
||||
|
||||
async Task Retry()
|
||||
{
|
||||
IsLoading = true;
|
||||
await LoadData();
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
void Reset() => rows = settings is null ? [] : InstanceAdminService.FormOf(settings).Rules;
|
||||
|
||||
void Add() => rows.Add(new ServerRuleForm());
|
||||
|
||||
// gone from the list, it is removed when the rules are saved (kept, marked, for the reports naming it)
|
||||
void Remove(ServerRuleForm row) => rows.Remove(row);
|
||||
|
||||
void Move(int position, int by)
|
||||
{
|
||||
var row = rows[position];
|
||||
rows.RemoveAt(position);
|
||||
rows.Insert(position + by, row);
|
||||
}
|
||||
|
||||
// listed again with its id, it is the same rule: the reports naming it name it still
|
||||
void Restore(ViewServerRule rule) => rows.Add(new ServerRuleForm { Id = rule.Id, Text = rule.Text, Hint = rule.Hint });
|
||||
|
||||
async Task Save()
|
||||
{
|
||||
var rules = rows.Select(r => new ServerRuleForm { Id = r.Id, Text = r.Text?.Trim(), Hint = string.IsNullOrWhiteSpace(r.Hint) ? null : r.Hint.Trim() }).ToList();
|
||||
isSaving = true;
|
||||
var result = await InstanceAdminService.SaveSettings(latest => latest.Rules = rules);
|
||||
isSaving = false;
|
||||
if (result.IsInvalid)
|
||||
{
|
||||
Toast.AddResponseError(result);
|
||||
return;
|
||||
}
|
||||
|
||||
settings = (ViewServerSettings)result.Data;
|
||||
Reset();
|
||||
Toast.AddResponseSuccess(Localizer["The rules are saved."]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user