Filters: made on /filters, applied by each card where it is

The persona's filters (PrivaPub's /api/v2/filters): made, given words and
whole-word rules, contexts, an action and an expiry on /filters (linked from
Settings), and deleted; posts filtered one by one can be taken out. Every
status says which filters it matches; each page tells its cards where they
are (home and lists, notifications, public feeds, the thread around the
opened post, profiles), and a card leaves its post out (hide), folds it
behind the filter's title with a way to show it anyway (warn), or blurs its
media (blur). The e2e test folds and then hides a post of the persona's own.

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-05 03:10:27 +02:00
1 parent e2491de16e
commit 9a752d9288
18 files changed
+1182 -240

No files matched your search

+248 -219
View File
@@ -5,247 +5,262 @@
about replies and deletions. data-post-* attributes tell the globe where the post is (wwwroot/js/globe.js); a
quoted post (Compact) has none, since it is not an item of the feed. A CDN-fronted server's post has no point, only
its CDN's group (data-cdn); data-post-at is when it was written, for the globe's Sun; data-uri is its ActivityPub id,
which the e2e tests (tests/e2e) find it by. Located posts have no actions: PrivaPub keeps them off the Mastodon API. Nerd stats are on
which the e2e tests (tests/e2e) find it by. The persona's filters apply where the page says the card is (FilterIn): a
hide filter leaves it out, a warn filter folds it behind the filter's title, a blur filter blurs its media. Located posts have no actions: PrivaPub keeps them off the Mastodon API. Nerd stats are on
the time, the author and its server's history, the place or the CDN, the media and the counts. *@
<article class="flex gap-3 w-full rounded-xl neo-raised @(Highlighted ? "neo-sm" : "neo-2xs") @CssContainer"
data-post-id="@(Compact ? null : Post.Key)" data-lat="@(Compact || Post.Location?.HasPoint != true ? null : Coordinate(Post.Location.Latitude))"
data-lng="@(Compact || Post.Location?.HasPoint != true ? null : Coordinate(Post.Location.Longitude))"
data-src="@(Compact ? null : Post.Location?.Source.ToString().ToLowerInvariant())" data-cdn="@(Compact ? null : Post.Location?.CdnKey)"
data-label="@(Compact ? null : Post.Location?.Label)" data-host="@(Compact ? null : Post.Host)"
data-range="@(Post.Located && !Compact ? Coordinate(Post.RangeKm) : null)" data-post-at="@(Compact ? null : Post.WrittenAt?.ToString("O"))"
data-uri="@(Compact ? null : Post.Status?.Uri)">
@if (!IsFilteredOut)
{
<article class="flex gap-3 w-full rounded-xl neo-raised @(Highlighted ? "neo-sm" : "neo-2xs") @CssContainer"
data-post-id="@(Compact ? null : Post.Key)" data-lat="@(Compact || Post.Location?.HasPoint != true ? null : Coordinate(Post.Location.Latitude))"
data-lng="@(Compact || Post.Location?.HasPoint != true ? null : Coordinate(Post.Location.Longitude))"
data-src="@(Compact ? null : Post.Location?.Source.ToString().ToLowerInvariant())" data-cdn="@(Compact ? null : Post.Location?.CdnKey)"
data-label="@(Compact ? null : Post.Location?.Label)" data-host="@(Compact ? null : Post.Host)"
data-range="@(Post.Located && !Compact ? Coordinate(Post.RangeKm) : null)" data-post-at="@(Compact ? null : Post.WrittenAt?.ToString("O"))"
data-uri="@(Compact ? null : Post.Status?.Uri)">
@if (Author is { } author)
{
<div class="flex flex-col py-3 pl-3 md:py-4 md:pl-4 flex-none rounded-l-xl bg-cover bg-no-repeat bg-center"
style="@HeaderBackground(author)">
<a class="block h-12 w-12 md:h-14 md:w-14" href="@($"account/{Uri.EscapeDataString(author.Id)}")" title="@author.Acct">
@* alt="": the author's name is next to it. *@
<img alt="" loading="lazy" class="h-12 w-12 md:h-14 md:w-14 object-cover rounded-full neo-raised neo-2xs bg-base-100"
src="@(author.Avatar ?? "imgs/icon-192.png")" />
</a>
</div>
}
<div class="flex flex-col gap-2 flex-1 py-3 pr-3 md:py-4 md:pr-4 min-w-0">
@if (Post.Notification is { } notification)
@if (Author is { } author)
{
<p class="inline-flex flex-wrap items-center gap-2 min-w-0 text-xs">
<i aria-hidden="true" class="@NotificationIcon(notification.Type)"></i>
<span>@NotificationText(notification)</span>
<span class="opacity-60" title="@NerdRow.Time(notification.CreatedAt)">@notification.CreatedAt.GetPassedTime(Localizer)</span>
</p>
}
else if (Post.BoostedBy is { } booster)
{
<p class="inline-flex flex-1 items-center gap-2 min-w-0 text-xs">
<i class="ion-md-repeat text-info" aria-hidden="true"></i>
<a class="font-bold truncate" href="@booster.Url" target="_blank" rel="noopener noreferrer" title="@booster.Acct">@booster.Name</a>
<span>@Localizer["boosted"]</span>
</p>
}
else if (Post.Conversation is { } conversation)
{
<p class="inline-flex flex-wrap items-center gap-2 min-w-0 text-xs">
<i class="ion-md-mail @(conversation.Unread ? "text-primary" : null)" aria-hidden="true"></i>
<span class="truncate">@string.Join(", ", conversation.Accounts.Select(a => "@" + a.Acct))</span>
</p>
<div class="flex flex-col py-3 pl-3 md:py-4 md:pl-4 flex-none rounded-l-xl bg-cover bg-no-repeat bg-center"
style="@HeaderBackground(author)">
<a class="block h-12 w-12 md:h-14 md:w-14" href="@($"account/{Uri.EscapeDataString(author.Id)}")" title="@author.Acct">
@* alt="": the author's name is next to it. *@
<img alt="" loading="lazy" class="h-12 w-12 md:h-14 md:w-14 object-cover rounded-full neo-raised neo-2xs bg-base-100"
src="@(author.Avatar ?? "imgs/icon-192.png")" />
</a>
</div>
}
@if (Status is { } status)
{
<header class="flex flex-wrap items-baseline gap-x-2 gap-y-1 min-w-0 justify-between text-xs md:text-sm">
<NerdTip Heading="@Localizer["Author"]" Rows="AuthorRows()" Pending="isReadingServer" OnOpen="ReadServer" Align="DTooltipAlign.Start" Class="min-w-0 flex-1">
<span class="inline-flex gap-2 min-w-0 max-w-full">
<b class="shrink truncate" title="@status.Account?.Name">@status.Account?.Name</b>
<span class="flex-1 min-w-6 opacity-50 truncate self-center text-xs">@("@" + status.Account?.Acct)</span>
</span>
</NerdTip>
<span class="flex-none inline-flex gap-2 items-center">
@if (Post.Location is { Source: LocationSource.Cdn } fronted)
{
<NerdTip Heading="@Localizer["The server is behind {0}, so its place is unknown.", fronted.Label]" Rows="CdnRows()" Pending="isReadingServer"
OnOpen="ReadServer" Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.End">
<span class="badge badge-ghost badge-sm gap-1 max-w-40">
<i class="ion-md-cloud" aria-hidden="true"></i>
<span class="truncate">@fronted.Label</span>
</span>
</NerdTip>
}
else if (Post.Location is { } location)
{
<NerdTip Heading="@Localizer["Place"]" Rows="LocationRows()" Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.End">
<span class="badge badge-ghost badge-sm gap-1 max-w-40">
<i class="@LocationIcon(location.Source)" aria-hidden="true"></i>
<span class="truncate">@(location.Label ?? Localizer["Near you"])</span>
</span>
</NerdTip>
}
<NerdTip Heading="@Localizer["Post"]" Rows="PostRows()" Pending="isReadingProvenance" OnOpen="ReadProvenance"
Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.End">
<time datetime="@NerdRow.Time(status.CreatedAt)" class="opacity-80">@status.CreatedAt.GetPassedTime(Localizer)</time>
@if (status.EditedAt is not null)
{
<span class="opacity-60">*</span>
}
</NerdTip>
<i aria-hidden="true" class="@VisibilityIcon(status.Visibility) text-base" title="@VisibilityText(status.Visibility)"></i>
</span>
</header>
<div class="flex flex-col gap-2 flex-1 py-3 pr-3 md:py-4 md:pr-4 min-w-0">
@if (status.Privapub?.Title is { Length: > 0 } title)
@if (Post.Notification is { } notification)
{
<p class="text-sm md:text-base font-bold break-words">@title</p>
<p class="inline-flex flex-wrap items-center gap-2 min-w-0 text-xs">
<i aria-hidden="true" class="@NotificationIcon(notification.Type)"></i>
<span>@NotificationText(notification)</span>
<span class="opacity-60" title="@NerdRow.Time(notification.CreatedAt)">@notification.CreatedAt.GetPassedTime(Localizer)</span>
</p>
}
@if (status.SpoilerText is { Length: > 0 } spoiler)
else if (Post.BoostedBy is { } booster)
{
<DCollapse @bind-IsOpen="isSpoilerOpen" Icon="DCollapseIcon.Arrow" class="rounded-lg neo-xs" HeaderClass="text-sm py-2 min-h-0"
ContentClass="text-sm">
<Header>
<span class="inline-flex gap-2 items-center"><i class="ion-md-warning text-warning"></i>@spoiler</span>
</Header>
<ChildContent>
@Body(status)
</ChildContent>
</DCollapse>
<p class="inline-flex flex-1 items-center gap-2 min-w-0 text-xs">
<i class="ion-md-repeat text-info" aria-hidden="true"></i>
<a class="font-bold truncate" href="@booster.Url" target="_blank" rel="noopener noreferrer" title="@booster.Acct">@booster.Name</a>
<span>@Localizer["boosted"]</span>
</p>
}
else
else if (Post.Conversation is { } conversation)
{
@Body(status)
}
@if (Post.Located)
{
<p class="inline-flex gap-2 items-center text-xs opacity-70">
<i class="ion-md-locate"></i>
<span>@Localizer["{0} km from you, shown within {1} km", Number(Post.DistanceKm), Number(Post.RangeKm)]</span>
<p class="inline-flex flex-wrap items-center gap-2 min-w-0 text-xs">
<i class="ion-md-mail @(conversation.Unread ? "text-primary" : null)" aria-hidden="true"></i>
<span class="truncate">@string.Join(", ", conversation.Accounts.Select(a => "@" + a.Acct))</span>
</p>
}
@if (!Post.Located && !Compact)
@if (Status is { } status)
{
<footer class="flex gap-2 mt-1 justify-between">
<div class="flex gap-2">
<DButton Size="DSize.Sm" Active="isReplying" Disabled="IsDefaultDisabled" OnClick="ToggleReply" title="@Localizer[isReplying ? "Close" : "Reply"]">
<i aria-hidden="true" class="@(isReplying ? "ion-md-close-circle" : "ion-md-text") text-lg text-success"></i>
@if (status.RepliesCount > 0)
{
<span>@status.RepliesCount</span>
}
</DButton>
<DButton Size="DSize.Sm" Active="status.Reblogged" Disabled="IsDefaultDisabled || isActing || !CanBoost(status)"
OnClick="() => Act(status.Reblogged ? StatusAction.Unreblog : StatusAction.Reblog)" title="@Localizer["Boost"]">
<i aria-hidden="true" class="ion-md-repeat text-lg text-info"></i>
@if (status.ReblogsCount > 0)
{
<span>@status.ReblogsCount</span>
}
</DButton>
<DButton Size="DSize.Sm" Active="status.Favourited" Disabled="IsDefaultDisabled || isActing"
OnClick="() => Act(status.Favourited ? StatusAction.Unfavourite : StatusAction.Favourite)" title="@Localizer["Favourite"]">
<i aria-hidden="true" class="@(status.Favourited ? "ion-md-heart" : "ion-md-heart-empty") text-lg text-pink-400"></i>
@if (status.FavouritesCount > 0)
{
<span>@status.FavouritesCount</span>
}
</DButton>
<DButton Size="DSize.Sm" Active="status.Bookmarked" Disabled="IsDefaultDisabled || isActing"
OnClick="() => Act(status.Bookmarked ? StatusAction.Unbookmark : StatusAction.Bookmark)" title="@Localizer["Bookmark"]">
<i aria-hidden="true" class="ion-md-bookmark text-lg @(status.Bookmarked ? "text-warning" : null)"></i>
</DButton>
<DDropdown @bind-IsOpen="isReactionsOpen" Placement="DDropdownPlacement.Top" CloseOnContentClick="true"
TriggerClass="@(isReactionsOpen ? "btn btn-sm btn-active" : "btn btn-sm")" ContentClass="mb-2 flex gap-1 p-2 rounded-box"
title="@Localizer["React"]">
<Trigger><i aria-hidden="true" class="ion-md-happy text-lg"></i></Trigger>
<ChildContent>
@foreach (var emoji in QuickReactions)
{
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => React(emoji, true)">@emoji</DButton>
}
</ChildContent>
</DDropdown>
<NerdTip Heading="@Localizer["Counts"]" Rows="CountRows(status)" Class="self-center">
<i class="ion-md-stats opacity-40" aria-label="@Localizer["Counts"]"></i>
</NerdTip>
</div>
<div class="flex gap-2">
<DDropdown @bind-IsOpen="isOptionsOpen" Align="DDropdownAlign.End" CloseOnContentClick="true" Placement="DDropdownPlacement.Top"
TriggerClass="@(isOptionsOpen ? "btn btn-sm btn-active" : "btn btn-sm")"
ContentClass="mb-2 flex flex-col gap-2 p-2 rounded-box" title="@Localizer["Other"]">
<Trigger>
<i aria-hidden="true" class="ion-md-more text-lg"></i>
</Trigger>
<ChildContent>
@if (status.Url is { Length: > 0 } url)
{
<a class="btn btn-sm justify-start whitespace-nowrap" href="@url" target="_blank" rel="noopener noreferrer">
<i aria-hidden="true" class="ion-md-open text-base"></i>
<span>@Localizer["Open original"]</span>
</a>
}
@if (!IsOwn)
{
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled || isActing" OnClick="@(() => AccountAction("mute"))">
<i aria-hidden="true" class="ion-md-eye-off text-base text-warning"></i>
<span>@Localizer["Mute"]</span>
</DButton>
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled || isActing" OnClick="@(() => AccountAction("block"))">
<i aria-hidden="true" class="ion-md-remove-circle text-base text-error"></i>
<span>@Localizer["Block"]</span>
</DButton>
}
else
{
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled || isActing" OnClick="Delete">
<i aria-hidden="true" class="ion-md-trash text-base text-error"></i>
<span>@Localizer["Delete"]</span>
</DButton>
}
</ChildContent>
</DDropdown>
@if (IncludeExpand)
<header class="flex flex-wrap items-baseline gap-x-2 gap-y-1 min-w-0 justify-between text-xs md:text-sm">
<NerdTip Heading="@Localizer["Author"]" Rows="AuthorRows()" Pending="isReadingServer" OnOpen="ReadServer" Align="DTooltipAlign.Start" Class="min-w-0 flex-1">
<span class="inline-flex gap-2 min-w-0 max-w-full">
<b class="shrink truncate" title="@status.Account?.Name">@status.Account?.Name</b>
<span class="flex-1 min-w-6 opacity-50 truncate self-center text-xs">@("@" + status.Account?.Acct)</span>
</span>
</NerdTip>
<span class="flex-none inline-flex gap-2 items-center">
@if (Post.Location is { Source: LocationSource.Cdn } fronted)
{
<NavLink class="btn btn-sm" href="@($"expand/{Uri.EscapeDataString(status.Id)}")" title="@Localizer["Expand"]">
<i aria-hidden="true" class="ion-md-expand text-lg"></i>
</NavLink>
<NerdTip Heading="@Localizer["The server is behind {0}, so its place is unknown.", fronted.Label]" Rows="CdnRows()" Pending="isReadingServer"
OnOpen="ReadServer" Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.End">
<span class="badge badge-ghost badge-sm gap-1 max-w-40">
<i class="ion-md-cloud" aria-hidden="true"></i>
<span class="truncate">@fronted.Label</span>
</span>
</NerdTip>
}
</div>
</footer>
else if (Post.Location is { } location)
{
<NerdTip Heading="@Localizer["Place"]" Rows="LocationRows()" Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.End">
<span class="badge badge-ghost badge-sm gap-1 max-w-40">
<i class="@LocationIcon(location.Source)" aria-hidden="true"></i>
<span class="truncate">@(location.Label ?? Localizer["Near you"])</span>
</span>
</NerdTip>
}
<NerdTip Heading="@Localizer["Post"]" Rows="PostRows()" Pending="isReadingProvenance" OnOpen="ReadProvenance"
Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.End">
<time datetime="@NerdRow.Time(status.CreatedAt)" class="opacity-80">@status.CreatedAt.GetPassedTime(Localizer)</time>
@if (status.EditedAt is not null)
{
<span class="opacity-60">*</span>
}
</NerdTip>
<i aria-hidden="true" class="@VisibilityIcon(status.Visibility) text-base" title="@VisibilityText(status.Visibility)"></i>
</span>
</header>
@if (isReplying)
@if (Warnings.Count > 0 && !showFiltered)
{
<Composer InReplyTo="status" OnPublished="Replied" />
<div class="flex flex-wrap items-center gap-2 text-sm rounded-lg neo-inset neo-2xs px-3 py-2" data-filtered>
<i class="ion-md-funnel opacity-70" aria-hidden="true"></i>
<span class="flex-1 min-w-0 break-words">@Localizer["Filtered: {0}", string.Join(", ", Warnings.Select(w => w.Filter.Title))]</span>
<DButton Size="DSize.Xs" OnClick="() => showFiltered = true">@Localizer["Show anyway"]</DButton>
</div>
}
else
{
@if (status.Privapub?.Title is { Length: > 0 } title)
{
<p class="text-sm md:text-base font-bold break-words">@title</p>
}
@if (status.SpoilerText is { Length: > 0 } spoiler)
{
<DCollapse @bind-IsOpen="isSpoilerOpen" Icon="DCollapseIcon.Arrow" class="rounded-lg neo-xs" HeaderClass="text-sm py-2 min-h-0"
ContentClass="text-sm">
<Header>
<span class="inline-flex gap-2 items-center"><i class="ion-md-warning text-warning"></i>@spoiler</span>
</Header>
<ChildContent>
@Body(status)
</ChildContent>
</DCollapse>
}
else
{
@Body(status)
}
@if (Post.Located)
{
<p class="inline-flex gap-2 items-center text-xs opacity-70">
<i class="ion-md-locate"></i>
<span>@Localizer["{0} km from you, shown within {1} km", Number(Post.DistanceKm), Number(Post.RangeKm)]</span>
</p>
}
@if (!Post.Located && !Compact)
{
<footer class="flex gap-2 mt-1 justify-between">
<div class="flex gap-2">
<DButton Size="DSize.Sm" Active="isReplying" Disabled="IsDefaultDisabled" OnClick="ToggleReply" title="@Localizer[isReplying ? "Close" : "Reply"]">
<i aria-hidden="true" class="@(isReplying ? "ion-md-close-circle" : "ion-md-text") text-lg text-success"></i>
@if (status.RepliesCount > 0)
{
<span>@status.RepliesCount</span>
}
</DButton>
<DButton Size="DSize.Sm" Active="status.Reblogged" Disabled="IsDefaultDisabled || isActing || !CanBoost(status)"
OnClick="() => Act(status.Reblogged ? StatusAction.Unreblog : StatusAction.Reblog)" title="@Localizer["Boost"]">
<i aria-hidden="true" class="ion-md-repeat text-lg text-info"></i>
@if (status.ReblogsCount > 0)
{
<span>@status.ReblogsCount</span>
}
</DButton>
<DButton Size="DSize.Sm" Active="status.Favourited" Disabled="IsDefaultDisabled || isActing"
OnClick="() => Act(status.Favourited ? StatusAction.Unfavourite : StatusAction.Favourite)" title="@Localizer["Favourite"]">
<i aria-hidden="true" class="@(status.Favourited ? "ion-md-heart" : "ion-md-heart-empty") text-lg text-pink-400"></i>
@if (status.FavouritesCount > 0)
{
<span>@status.FavouritesCount</span>
}
</DButton>
<DButton Size="DSize.Sm" Active="status.Bookmarked" Disabled="IsDefaultDisabled || isActing"
OnClick="() => Act(status.Bookmarked ? StatusAction.Unbookmark : StatusAction.Bookmark)" title="@Localizer["Bookmark"]">
<i aria-hidden="true" class="ion-md-bookmark text-lg @(status.Bookmarked ? "text-warning" : null)"></i>
</DButton>
<DDropdown @bind-IsOpen="isReactionsOpen" Placement="DDropdownPlacement.Top" CloseOnContentClick="true"
TriggerClass="@(isReactionsOpen ? "btn btn-sm btn-active" : "btn btn-sm")" ContentClass="mb-2 flex gap-1 p-2 rounded-box"
title="@Localizer["React"]">
<Trigger><i aria-hidden="true" class="ion-md-happy text-lg"></i></Trigger>
<ChildContent>
@foreach (var emoji in QuickReactions)
{
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => React(emoji, true)">@emoji</DButton>
}
</ChildContent>
</DDropdown>
<NerdTip Heading="@Localizer["Counts"]" Rows="CountRows(status)" Class="self-center">
<i class="ion-md-stats opacity-40" aria-label="@Localizer["Counts"]"></i>
</NerdTip>
</div>
<div class="flex gap-2">
<DDropdown @bind-IsOpen="isOptionsOpen" Align="DDropdownAlign.End" CloseOnContentClick="true" Placement="DDropdownPlacement.Top"
TriggerClass="@(isOptionsOpen ? "btn btn-sm btn-active" : "btn btn-sm")"
ContentClass="mb-2 flex flex-col gap-2 p-2 rounded-box" title="@Localizer["Other"]">
<Trigger>
<i aria-hidden="true" class="ion-md-more text-lg"></i>
</Trigger>
<ChildContent>
@if (status.Url is { Length: > 0 } url)
{
<a class="btn btn-sm justify-start whitespace-nowrap" href="@url" target="_blank" rel="noopener noreferrer">
<i aria-hidden="true" class="ion-md-open text-base"></i>
<span>@Localizer["Open original"]</span>
</a>
}
@if (!IsOwn)
{
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled || isActing" OnClick="@(() => AccountAction("mute"))">
<i aria-hidden="true" class="ion-md-eye-off text-base text-warning"></i>
<span>@Localizer["Mute"]</span>
</DButton>
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled || isActing" OnClick="@(() => AccountAction("block"))">
<i aria-hidden="true" class="ion-md-remove-circle text-base text-error"></i>
<span>@Localizer["Block"]</span>
</DButton>
}
else
{
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled || isActing" OnClick="Delete">
<i aria-hidden="true" class="ion-md-trash text-base text-error"></i>
<span>@Localizer["Delete"]</span>
</DButton>
}
</ChildContent>
</DDropdown>
@if (IncludeExpand)
{
<NavLink class="btn btn-sm" href="@($"expand/{Uri.EscapeDataString(status.Id)}")" title="@Localizer["Expand"]">
<i aria-hidden="true" class="ion-md-expand text-lg"></i>
</NavLink>
}
</div>
</footer>
@if (isReplying)
{
<Composer InReplyTo="status" OnPublished="Replied" />
}
}
}
}
}
else if (Post.Notification?.Account is { } follower)
{
<p class="text-sm">
<a class="font-bold" href="@($"account/{Uri.EscapeDataString(follower.Id)}")">@follower.Name</a>
<span class="opacity-60">@("@" + follower.Acct)</span>
</p>
@if (Post.Notification.Type == "follow_request" && requestAnswer is null)
else if (Post.Notification?.Account is { } follower)
{
<div class="flex gap-2">
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => AnswerRequest(follower, true)">
<i class="ion-md-checkmark"></i><span>@Localizer["Accept"]</span>
</DButton>
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => AnswerRequest(follower, false)">
<i class="ion-md-close"></i><span>@Localizer["Refuse"]</span>
</DButton>
</div>
<p class="text-sm">
<a class="font-bold" href="@($"account/{Uri.EscapeDataString(follower.Id)}")">@follower.Name</a>
<span class="opacity-60">@("@" + follower.Acct)</span>
</p>
@if (Post.Notification.Type == "follow_request" && requestAnswer is null)
{
<div class="flex gap-2">
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => AnswerRequest(follower, true)">
<i class="ion-md-checkmark"></i><span>@Localizer["Accept"]</span>
</DButton>
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => AnswerRequest(follower, false)">
<i class="ion-md-close"></i><span>@Localizer["Refuse"]</span>
</DButton>
</div>
}
else if (requestAnswer is { } accepted)
{
<p class="text-xs opacity-70">@(accepted ? Localizer["Accepted."] : Localizer["Refused."])</p>
}
}
else if (requestAnswer is { } accepted)
{
<p class="text-xs opacity-70">@(accepted ? Localizer["Accepted."] : Localizer["Refused."])</p>
}
}
</div>
</div>
</article>
</article>
}
@code {
[Inject] StatusService StatusService { get; set; }
@@ -262,11 +277,15 @@
[Parameter] public bool IncludeExpand { get; set; } = true;
/// <summary>Without actions, e.g. a quoted post.</summary>
[Parameter] public bool Compact { get; set; }
/// <summary>Where the card is shown, for the persona's filters (Status.Filtered): a hide filter of that context leaves
/// it out, a warn filter folds it, a blur filter blurs its media. No filter applies without it.</summary>
[Parameter] public FilterContext? FilterIn { get; set; }
bool isReplying;
bool isActing;
bool isOptionsOpen;
bool isSpoilerOpen;
bool showFiltered;
bool isReadingProvenance;
bool isReadingServer;
readonly HashSet<string> revealedMedia = [];
@@ -277,6 +296,16 @@
InstanceLocation ServerGeo => Server?.Geo;
bool IsOwn => Post.IsOwnedBy(CascadingState.PersonaId);
List<FilterResult> Matched => FilterIn is { } where && Status is { } status
? [.. status.Filtered.Where(r => r.Filter is { IsActive: true } filter && filter.Context.Contains(where))]
: [];
bool IsFilteredOut => Matched.Any(r => r.Filter.FilterAction == FilterAction.Hide);
List<FilterResult> Warnings => [.. Matched.Where(r => r.Filter.FilterAction == FilterAction.Warn)];
bool IsBlurred => !showFiltered && Matched.Any(r => r.Filter.FilterAction == FilterAction.Blur);
protected override void OnInitialized() => IsLoading = false;
RenderFragment Body(Status status) => @<div class="flex flex-col gap-3">
@@ -291,11 +320,11 @@
@foreach (var media in status.MediaAttachments)
{
<NerdTip Heading="@Localizer["Media"]" Rows="MediaRows(media)" Class="block w-full">
@if (status.Sensitive && !revealedMedia.Contains(media.Id))
@if ((status.Sensitive || IsBlurred) && !revealedMedia.Contains(media.Id))
{
<button type="button" class="flex items-center justify-center w-full aspect-video rounded-lg neo-inset neo-2xs text-sm"
@onclick="() => revealedMedia.Add(media.Id)">
<i class="ion-md-eye-off mr-2"></i>@Localizer["Sensitive media, show"]
<i class="ion-md-eye-off mr-2"></i>@(IsBlurred ? Localizer["Filtered media, show"] : Localizer["Sensitive media, show"])
</button>
}
else