R4 the client catches up: follow requests, followers, downvotes, dismissing, filtering a post, quote policies, statistics in detail
Build / Build (push) Successful in 1m8s
Build / Build (push) Successful in 1m8s
Follow requests answered on the persona's own profile and from the Notifications tab; its followers and the accounts it follows listed there, a follower removed (asked first). Downvotes on threadiverse posts, after the public-vote notice. A notification dismissed from its line. "Filter this post" from a card's menu, and the Filters page showing the posts a filter holds. Who may quote the persona's own posts, and revoking a quote of one. Statistics: a server in detail with "describe again", the ledger's events, the server's own days. The composer deletes an upload taken out before posting. MediaType.Documents goes. 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
b731d8c8c0
commit
b8d7858acb
25 files changed
+1543
-61
No files matched your search
+61
-6
@@ -6,7 +6,9 @@
|
||||
@* The current persona's filters (/api/v2/filters): words, phrases and posts it would rather not see, where (home and
|
||||
lists, notifications, the public feeds, threads, profiles), how (folded behind the filter's title, left out, or with
|
||||
media blurred) and until when. PrivaPub marks every post with the filters it matches; the cards apply them
|
||||
(Content.FilterIn). Filters are the persona's own and never leave PrivaPub. *@
|
||||
(Content.FilterIn), and a card's "Filter this post" adds a post to one. The posts a filter holds are read by id when it
|
||||
opens (/api/v1/statuses?id[]=) and show their author and words, linking their thread. Filters are the persona's own
|
||||
and never leave PrivaPub. *@
|
||||
<section class="block relative w-full h-full neo-inset neo-xs rounded-xl">
|
||||
<div class="flex flex-col gap-4 p-4 md:p-5 w-full h-full absolute overflow-y-auto">
|
||||
|
||||
@@ -124,11 +126,33 @@
|
||||
@if (filter.Statuses.Count > 0)
|
||||
{
|
||||
<h2 class="font-semibold text-sm">@Localizer["Posts filtered one by one"]</h2>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-col gap-2">
|
||||
@foreach (var item in filter.Statuses)
|
||||
{
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<a class="link flex-1 min-w-0 truncate" href="@($"expand/{Uri.EscapeDataString(item.StatusId)}")">@item.StatusId</a>
|
||||
<div class="flex items-center gap-2 text-sm" data-filter-status="@item.StatusId">
|
||||
@if (PostOf(item) is { } post)
|
||||
{
|
||||
<a class="flex flex-1 items-center gap-2 min-w-0 rounded-lg p-2 neo-inset neo-2xs pointer-coarse:min-h-11"
|
||||
href="@($"expand/{Uri.EscapeDataString(post.Id)}")">
|
||||
@* alt="": the name is next to it. *@
|
||||
<img alt="" class="h-8 w-8 flex-none object-cover rounded-full" src="@(post.Account?.Avatar ?? "imgs/icon-192.png")" />
|
||||
<span class="flex flex-col min-w-0">
|
||||
<span class="truncate text-xs">
|
||||
<b>@CustomEmojis.Text(post.Account?.Name, post.Account?.Emojis ?? [])</b>
|
||||
<span class="opacity-60">@("@" + post.Account?.Acct)</span>
|
||||
</span>
|
||||
<span class="truncate opacity-80">@(Excerpt(post) is { Length: > 0 } excerpt ? excerpt : Localizer["Media"])</span>
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
else if (readPostIds.Contains(item.StatusId))
|
||||
{
|
||||
<span class="flex-1 min-w-0 opacity-70 italic" data-filter-status-missing>@Localizer["Post no longer available"]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="flex-1 min-w-0"><DLoading Size="DSize.Xs" LabelText="@Localizer["loading"]" /></span>
|
||||
}
|
||||
<DButton Size="DSize.Xs" Disabled="@(IsDefaultDisabled || isSaving)" OnClick="@(() => RemoveStatus(filter, item))">
|
||||
<i class="ion-md-close"></i><span class="hidden sm:inline">@Localizer["Remove"]</span>
|
||||
</DButton>
|
||||
@@ -170,6 +194,7 @@
|
||||
static readonly FilterAction[] Actions = [FilterAction.Warn, FilterAction.Hide, FilterAction.Blur];
|
||||
|
||||
[Inject] FilterService FilterService { get; set; }
|
||||
[Inject] TimelineService TimelineService { get; set; }
|
||||
|
||||
List<Filter> filters = [];
|
||||
TitleForm draft = new();
|
||||
@@ -178,6 +203,9 @@
|
||||
string loadedPersonaId;
|
||||
bool isSaving;
|
||||
bool isConfirmingDelete;
|
||||
// the posts the filters hold, read by id when a filter opens; an id read but missing is a post no longer available
|
||||
Dictionary<string, Status> filteredPosts = new();
|
||||
HashSet<string> readPostIds = [];
|
||||
|
||||
(int Seconds, string Label)[] Expiries =>
|
||||
[
|
||||
@@ -240,6 +268,8 @@
|
||||
{
|
||||
IsLoading = true;
|
||||
open = null;
|
||||
filteredPosts = new();
|
||||
readPostIds = [];
|
||||
filters = await FilterService.All();
|
||||
IsLoading = false;
|
||||
}
|
||||
@@ -260,10 +290,10 @@
|
||||
filters.Add(filter);
|
||||
draft = new();
|
||||
Toast.AddResponseSuccess(Localizer["Filter {0} made.", filter.Title]);
|
||||
Toggle(filter);
|
||||
await Toggle(filter);
|
||||
}
|
||||
|
||||
void Toggle(Filter filter)
|
||||
async Task Toggle(Filter filter)
|
||||
{
|
||||
isConfirmingDelete = false;
|
||||
if (open?.Id == filter.Id)
|
||||
@@ -281,6 +311,31 @@
|
||||
ExpiresIn = KeepExpiry,
|
||||
Keywords = [.. filter.Keywords.Select(k => new KeywordRow { Id = k.Id, Keyword = k.Keyword, WholeWord = k.WholeWord, WasWholeWord = k.WholeWord })]
|
||||
};
|
||||
await ReadPosts(filter);
|
||||
}
|
||||
|
||||
// its posts not read yet, 20 a request (TimelineService.Read); the ones that are gone stay missing
|
||||
async Task ReadPosts(Filter filter)
|
||||
{
|
||||
var ids = filter.Statuses.Select(s => s.StatusId).Where(id => id is not null && !readPostIds.Contains(id)).Distinct().ToList();
|
||||
if (ids.Count == 0)
|
||||
return;
|
||||
foreach (var (id, status) in await TimelineService.Read(ids))
|
||||
filteredPosts[id] = status;
|
||||
readPostIds.UnionWith(ids);
|
||||
}
|
||||
|
||||
Status PostOf(FilterStatus item) => item.StatusId is { } id ? filteredPosts.GetValueOrDefault(id) : null;
|
||||
|
||||
// what the post says, as plain text: its content warning when it has one, else the start of its words
|
||||
static string Excerpt(Status status)
|
||||
{
|
||||
var html = status.SpoilerText is { Length: > 0 } warning ? warning : status.Content;
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return null;
|
||||
var text = System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", " ");
|
||||
text = System.Net.WebUtility.HtmlDecode(System.Text.RegularExpressions.Regex.Replace(text, @"\s+", " ")).Trim();
|
||||
return text.Length <= 160 ? text : text[..160].TrimEnd() + "…";
|
||||
}
|
||||
|
||||
void ToggleContext(FilterContext context, bool on)
|
||||
|
||||
Reference in new issue
Block a user