From 1d12dff033624df441df746279e8791ff7a7692f Mon Sep 17 00:00:00 2001 From: thepra Date: Mon, 5 Oct 2026 04:17:27 +0200 Subject: [PATCH] A notification told alone names its group v1 notifications, and those a stream tells, carried an 'ungrouped' key, so a client reading grouped notifications could not fold a new like into the group of likes it already shows. Each now names the group it belongs to by default (likes and boosts of one post, follows within an hour). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- PrivaPub.Tests/Http/OneAnswerTests.cs | 2 ++ PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs | 4 ++-- PrivaPub/Api/Mastodon/Mappers/MastodonMapper.cs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/PrivaPub.Tests/Http/OneAnswerTests.cs b/PrivaPub.Tests/Http/OneAnswerTests.cs index 564c9d1..c5f42b2 100644 --- a/PrivaPub.Tests/Http/OneAnswerTests.cs +++ b/PrivaPub.Tests/Http/OneAnswerTests.cs @@ -167,6 +167,8 @@ namespace PrivaPub.Tests.Http var key = group.Text("group_key"); Assert.Equal($"grouped-favourite-{id}", key); + // told alone (v1, a stream), each notification names the group it belongs to + Assert.All((await author.Client.Get("/api/v1/notifications?types[]=favourite")).Ok().Array, n => Assert.Equal(key, n.Text("group_key"))); Assert.Equal(2, group.Number("notifications_count")); Assert.Equal(2, group["sample_account_ids"]!.AsArray().Count); Assert.Equal(id, group.Text("status_id")); diff --git a/PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs b/PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs index d629e73..506f645 100644 --- a/PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs +++ b/PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs @@ -206,11 +206,11 @@ namespace PrivaPub.Api.Mastodon.Controllers // Grouped notifications (Mastodon 4.3): api_versions.mastodon says we speak that API, so clients call it. Likes and // boosts of one post group together, as do follows within an hour; everything else stands alone. - static readonly HashSet DefaultGrouped = new() { "favourite", "reblog", "follow" }; + internal static readonly HashSet DefaultGrouped = new() { "favourite", "reblog", "follow" }; HashSet Grouped() => Params.List("grouped_types") is { Count: > 0 } chosen ? chosen.ToHashSet() : DefaultGrouped; - static string GroupKey(NotificationEntity n, ISet grouped) => + internal static string GroupKey(NotificationEntity n, ISet grouped) => !grouped.Contains(Names[n.Type]) ? $"ungrouped-{n.ID}" : n.Type == NotificationType.Follow ? $"grouped-follow-{n.CreatedAt:yyyyMMddHH}" : string.IsNullOrEmpty(n.PostId) ? $"ungrouped-{n.ID}" diff --git a/PrivaPub/Api/Mastodon/Mappers/MastodonMapper.cs b/PrivaPub/Api/Mastodon/Mappers/MastodonMapper.cs index be5ebfc..f325496 100644 --- a/PrivaPub/Api/Mastodon/Mappers/MastodonMapper.cs +++ b/PrivaPub/Api/Mastodon/Mappers/MastodonMapper.cs @@ -56,7 +56,8 @@ namespace PrivaPub.Api.Mastodon.Mappers Id = n.ID, Type = Controllers.NotificationsController.Names[n.Type], CreatedAt = MastodonJson.Time(n.CreatedAt), - GroupKey = $"ungrouped-{n.ID}", + //the group it belongs to by default, so a notification told alone (v1, a stream) joins its group in a client + GroupKey = Controllers.NotificationsController.GroupKey(n, Controllers.NotificationsController.DefaultGrouped), Account = accounts[n.FromAccountId], Status = n.PostId == default ? default : statuses[n.PostId], Emoji = n.Emoji == default ? default : n.EmojiURL == default ? n.Emoji : $":{n.Emoji}:",