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:
1 parent
e2491de16e
commit
9a752d9288
18 files changed
+1182
-240
No files matched your search
+248
-219
@@ -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
|
||||
|
||||
Reference in new issue
Block a user