Conversations page, and know what is unread

/api/v1/conversations answered one page, never unread, its read endpoint
did nothing, and DELETE was missing; each conversation cost a query per
member. Now each conversation keeps its newest post (DmGroup.LastPostId,
set as posts arrive, learnt once by migration _013) and pages by it as
Mastodon does, and each persona's ConversationState holds what it read and
what it took off its list:

- unread when someone else wrote last, after what the persona read;
- read marks it so, and writing in a conversation reads it;
- DELETE takes it off the list until a newer message brings it back.

The list reads its states, newest posts, members and accounts in a few
queries per page.

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 05:37:28 +02:00
1 parent ea5e607779
commit 17922f4dfd
11 files changed
+214 -27

No files matched your search

+6
View File
@@ -54,6 +54,12 @@ namespace PrivaPub.Infrastructure.Data
await Plain<Group>(token, g => g.InvitationCode);
await Plain<DmGroup>(token, g => g.ParticipantsKey);
await Plain<DmGroup>(token, g => g.LastPostId);
await DB.Default.Index<ConversationState>()
.Key(s => s.AvatarId, KeyType.Ascending)
.Key(s => s.ConversationId, KeyType.Ascending)
.Option(o => o.Unique = true)
.CreateAsync(token);
await Unique<GroupAnnouncement>(a => a.ActivityURI, Builders<GroupAnnouncement>.Filter.Type(a => a.ActivityURI, BsonType.String), token);
await Plain<Delivery>(token, d => d.DeliveredAt, d => d.AbandonedAt, d => d.NextAttemptAt);
@@ -0,0 +1,24 @@
using MongoDB.Entities;
using PrivaPub.Models.Group;
using PostEntity = PrivaPub.Models.Post.Post;
namespace PrivaPub.Infrastructure.Data.Migrations
{
// the conversations list pages by each conversation's newest post (DmGroup.LastPostId), set from now on as posts
// arrive; the conversations made before learn theirs once
public class _013_conversations_know_their_newest_post : IMigration
{
public async Task UpgradeAsync()
{
foreach (var group in await DB.Default.Find<DmGroup>().Match(g => g.LastPostId == null).ExecuteAsync())
{
var newest = await DB.Default.Find<PostEntity>().Match(p => p.ConversationId == group.ID && !p.DeletedAt.HasValue)
.Sort(p => p.ID, Order.Descending).ExecuteFirstAsync();
if (newest != default)
await DB.Default.Update<DmGroup>().MatchID(group.ID).Modify(g => g.LastPostId, newest.ID).ExecuteAsync();
}
}
}
}