The persona's notification policy, and what it filtered

Settings has a Notifications section, also at /notifications/filtered: what notifications from people the persona does
not follow, people who do not follow it, new accounts, private mentions it did not ask for and limited accounts become
(accept, filter, drop), and the filtered ones, one request per account, let in (with that account from then on) or
dismissed. The Notifications tab says when requests wait. e2e: a stranger's like waits in a request reached from the
tab and comes back once let in; the policy and the stranger's permission are put back afterwards (Pasture.Mongo).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 16:47:51 +02:00
1 parent a0f07abf8c
commit 658683f816
13 files changed
+467

No files matched your search

@@ -0,0 +1,149 @@
@using Microsoft.AspNetCore.Components.Rendering
@inherits LocalizableComponentBase
@* The persona's notification policy (/api/v2/notifications/policy): what notifications from people it does not follow,
people who do not follow it, new accounts, private mentions it did not ask for and accounts this server limits become.
Filtered ones wait in a request per account (/api/v1/notifications/requests), let in (with that account from then
on) or dismissed here. *@
@if (IsLoading)
{
<p class="flex w-full items-center justify-center gap-2">
<DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />
@Localizer["loading"]
</p>
}
else
{
<div class="flex flex-col gap-4" data-notification-policy>
<EditForm Model="policy" OnValidSubmit="Save" class="flex flex-col gap-2">
<label class="flex flex-wrap items-center justify-between gap-2">
<span class="text-sm">@Localizer["People you don't follow"]</span>
<DSelect TValue="string" Size="DSize.Sm" class="w-40" @bind-Value="policy.ForNotFollowing" data-policy="for_not_following">@Actions</DSelect>
</label>
<label class="flex flex-wrap items-center justify-between gap-2">
<span class="text-sm">@Localizer["People who don't follow you"]</span>
<DSelect TValue="string" Size="DSize.Sm" class="w-40" @bind-Value="policy.ForNotFollowers" data-policy="for_not_followers">@Actions</DSelect>
</label>
<label class="flex flex-wrap items-center justify-between gap-2">
<span class="text-sm">@Localizer["New accounts"]</span>
<DSelect TValue="string" Size="DSize.Sm" class="w-40" @bind-Value="policy.ForNewAccounts" data-policy="for_new_accounts">@Actions</DSelect>
</label>
<label class="flex flex-wrap items-center justify-between gap-2">
<span class="text-sm">@Localizer["Private mentions you did not ask for"]</span>
<DSelect TValue="string" Size="DSize.Sm" class="w-40" @bind-Value="policy.ForPrivateMentions" data-policy="for_private_mentions">@Actions</DSelect>
</label>
<label class="flex flex-wrap items-center justify-between gap-2">
<span class="text-sm">@Localizer["Accounts this server limits"]</span>
<DSelect TValue="string" Size="DSize.Sm" class="w-40" @bind-Value="policy.ForLimitedAccounts" data-policy="for_limited_accounts">@Actions</DSelect>
</label>
<p class="text-xs opacity-70">@Localizer["Filtered notifications wait below, one request per account; dropped ones are never kept."]</p>
<DButton Type="submit" Size="DSize.Sm" Color="DColor.Primary" class="self-start" Disabled="@(IsDefaultDisabled || isSaving)" data-policy-save>
@Localizer["Save"]
</DButton>
</EditForm>
<section class="flex flex-col gap-2" data-notification-requests>
<h3 class="font-semibold text-sm">@Localizer["Filtered notifications"]</h3>
@if (requests.Count == 0)
{
<p class="text-sm opacity-70">@Localizer["No filtered notifications."]</p>
}
@foreach (var request in requests)
{
<div class="flex flex-col gap-2 p-3 rounded-xl neo-raised neo-2xs min-w-0" data-request="@request.Account.Acct">
<div class="flex flex-wrap items-center gap-3 min-w-0">
<a class="flex items-center gap-3 min-w-0 flex-1" href="@($"account/{Uri.EscapeDataString(request.Account.Id)}")">
@* alt="": the name is next to it. *@
<img alt="" class="h-9 w-9 flex-none object-cover rounded-full" src="@(request.Account.Avatar ?? "imgs/icon-192.png")" />
<span class="flex flex-col min-w-0">
<b class="truncate">@CustomEmojis.Text(request.Account.Name, request.Account.Emojis)</b>
<span class="text-xs opacity-70 truncate">@("@" + request.Account.Acct)</span>
</span>
</a>
<DBadge Size="DSize.Sm">@Localizer["{0} notifications", request.NotificationsCount]</DBadge>
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="@(IsDefaultDisabled || busy.Contains(request.Id))"
OnClick="@(() => Answer(request, true))" data-request-accept>@Localizer["Let in"]</DButton>
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || busy.Contains(request.Id))"
OnClick="@(() => Answer(request, false))" data-request-dismiss>@Localizer["Dismiss"]</DButton>
</div>
@if (request.LastStatus?.Content is { Length: > 0 } content)
{
<div class="post-content text-sm opacity-80 break-words line-clamp-3">@((MarkupString)CustomEmojis.Html(content, request.LastStatus.Emojis))</div>
}
</div>
}
</section>
</div>
}
@code {
[Inject] TimelineService TimelineService { get; set; }
NotificationPolicy policy = new();
// what a notification can become, the same for every rule
void Actions(RenderTreeBuilder __builder)
{
<option value="accept">@Localizer["Accept"]</option>
<option value="filter">@Localizer["Filter"]</option>
<option value="drop">@Localizer["Drop"]</option>
}
List<NotificationRequest> requests = [];
// the requests whose answer is on its way
HashSet<string> busy = [];
bool isSaving;
string loadedPersonaId;
protected override async Task OnParametersSetAsync()
{
if (CascadingState.PersonaId == loadedPersonaId)
return;
loadedPersonaId = CascadingState.PersonaId;
if (CascadingState.IsOffline)
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
await LoadData();
IsLoading = false;
}
async Task LoadData()
{
policy = await TimelineService.NotificationPolicy() ?? new();
requests = await TimelineService.NotificationRequests();
}
async Task Save()
{
isSaving = true;
var result = await TimelineService.SetNotificationPolicy(policy);
isSaving = false;
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
policy = (NotificationPolicy)result.Data ?? policy;
Toast.AddResponseSuccess(Localizer["Notification policy saved."]);
}
async Task Answer(NotificationRequest request, bool accept)
{
busy.Add(request.Id);
var result = await TimelineService.AnswerNotificationRequest(request.Id, accept);
busy.Remove(request.Id);
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
requests.Remove(request);
}
}