Notifications in groups, and what is unread
The Notifications tab reads grouped notifications (/api/v2/notifications): likes and boosts of one post, or follows of one hour, are one card naming both accounts of a pair, or the newest and how many others, with their avatars. A notification the stream tells joins its group and waits on top. The tab's badge counts what is unread; opening the tab moves the notifications marker to the newest shown, which reads it on PrivaPub. 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
963565b316
commit
a4d9831691
11 files changed
+263
-12
No files matched your search
+38
-3
@@ -18,7 +18,7 @@
|
||||
</aside>
|
||||
|
||||
<section class="flex flex-col gap-3 min-h-0 flex-1 lg:h-full">
|
||||
<FeedTabs Filter="Filter" Groups="groups" Lists="lists" Stats="FeedStats()" IsComposing="isComposing" OnCompose="() => isComposing = !isComposing"
|
||||
<FeedTabs Filter="Filter" Groups="groups" Lists="lists" UnreadNotifications="unreadNotifications" Stats="FeedStats()" IsComposing="isComposing" OnCompose="() => isComposing = !isComposing"
|
||||
OnChanged="ChangeFeed" />
|
||||
|
||||
<div class="relative flex-1 min-h-0 neo-inset neo-xs rounded-xl">
|
||||
@@ -115,6 +115,7 @@
|
||||
List<FeedPost> pending = [];
|
||||
List<ViewGroup> groups = [];
|
||||
List<MastodonList> lists = [];
|
||||
int unreadNotifications;
|
||||
string maxId;
|
||||
string minId;
|
||||
string loadError;
|
||||
@@ -245,6 +246,7 @@
|
||||
minId = page.MinId;
|
||||
lastPollAt = DateTime.UtcNow;
|
||||
await Streaming.Follow(StreamOf(Filter), cancellationToken);
|
||||
await ReadNotifications(cancellationToken);
|
||||
StateHasChanged();
|
||||
await Locate(posts, cancellationToken);
|
||||
if (feedModule is not null)
|
||||
@@ -329,6 +331,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>On the Notifications tab, what is shown is read (the marker moves to the newest); elsewhere the tab's badge
|
||||
/// counts what is unread.</summary>
|
||||
async Task ReadNotifications(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Filter.TimelineType is not TimelineType.Notifications)
|
||||
{
|
||||
unreadNotifications = await TimelineService.UnreadNotifications(cancellationToken);
|
||||
return;
|
||||
}
|
||||
var newest = posts.Concat(pending).Select(p => p.Notification?.Id).Where(id => id is not null).Max(StringComparer.Ordinal);
|
||||
if (newest is not null)
|
||||
await TimelineService.ReadNotificationsUpTo(newest, cancellationToken);
|
||||
unreadNotifications = 0;
|
||||
}
|
||||
|
||||
/// <summary>The stream that tells this feed's new posts, or null for a feed PrivaPub does not stream.</summary>
|
||||
static StreamSubscription StreamOf(FeedFilter filter) => filter.TimelineType switch
|
||||
{
|
||||
@@ -348,12 +365,29 @@
|
||||
switch (message.Event)
|
||||
{
|
||||
case "update" when message.Status is { } status && Filter.TimelineType is not TimelineType.Notifications:
|
||||
case "notification" when message.Notification is not null && Filter.TimelineType is TimelineType.Notifications:
|
||||
var post = message.Status is { } s ? FeedPost.Of(s) : FeedPost.Of(message.Notification);
|
||||
var post = FeedPost.Of(status);
|
||||
if (posts.Concat(pending).Any(p => p.Key == post.Key))
|
||||
return;
|
||||
pending.Insert(0, post);
|
||||
break;
|
||||
case "notification" when message.Notification is { } notification && Filter.TimelineType is TimelineType.Notifications:
|
||||
// a notification of a group already shown joins it, and the group waits on top as new
|
||||
var key = notification.GroupKey ?? notification.Id;
|
||||
var group = pending.FirstOrDefault(p => p.Key == key) ?? posts.FirstOrDefault(p => p.Key == key);
|
||||
if (group is null)
|
||||
{
|
||||
pending.Insert(0, FeedPost.Of(notification));
|
||||
break;
|
||||
}
|
||||
group.GroupCount++;
|
||||
group.Notification = notification;
|
||||
group.GroupAccounts = [.. group.GroupAccounts.Prepend(notification.Account).Where(a => a is not null).DistinctBy(a => a.Id).Take(8)];
|
||||
if (posts.Remove(group))
|
||||
pending.Insert(0, group);
|
||||
break;
|
||||
case "notification" when Filter.TimelineType is not TimelineType.Notifications:
|
||||
unreadNotifications++;
|
||||
break;
|
||||
case "delete" when message.DeletedId is { } id:
|
||||
posts.RemoveAll(p => p.Key == id || p.Status?.Id == id);
|
||||
pending.RemoveAll(p => p.Key == id || p.Status?.Id == id);
|
||||
@@ -373,6 +407,7 @@
|
||||
var shown = pending;
|
||||
pending = [];
|
||||
posts.InsertRange(0, shown);
|
||||
await ReadNotifications(loadCancellation?.Token ?? default);
|
||||
minId = shown.FirstOrDefault()?.CursorId ?? minId;
|
||||
await feedModule.InvokeVoidAsync("scrollToTop", ScrollId);
|
||||
await Locate(shown, loadCancellation?.Token ?? default);
|
||||
|
||||
Reference in new issue
Block a user