mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
fix: Concord unread badge + last-message previews ignore thread replies
The unread count, the channel-list/hub last-message previews, and the Messages-row summary all read a broader note set than the channel feed renders: the feed's ChannelFeedFilter hides kind-1111 CommentEvent thread replies (and unacceptable authors), but these row surfaces counted/showed them. A trailing minichat reply therefore stuck the unread badge at a count opening the channel could never clear (markAsRead is monotonic and only advances for rendered timeline messages), and showed up as a "last message" that isn't on the timeline. Centralizes the feed's predicate as isConcordTimelineMessage (loaded, acceptable, not a CommentEvent) plus a newestTimelineNote helper, and routes the unread count, both list-row previews, and the Messages hub filter through it — so every channel-row summary agrees with what the open channel renders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b8966049b8
commit
18f1d87492
+3
-1
@@ -338,7 +338,9 @@ private fun ConcordChannelListRow(
|
||||
.flow()
|
||||
.notes.stateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
val lastNote = channelState.channel.lastNote
|
||||
// The newest *timeline* message (not the raw lastNote): skips kind-1111 thread replies and
|
||||
// hidden authors so the preview + time match the channel feed and the unread badge below.
|
||||
val lastNote = remember(channelState) { channel.newestTimelineNote(account) }
|
||||
val unread by
|
||||
remember(communityId, channelKey) { concordChannelUnreadCountFlow(account, communityId, channelKey) }
|
||||
.collectAsStateWithLifecycle(0)
|
||||
|
||||
+3
-1
@@ -402,7 +402,9 @@ private fun ConcordChannelRow(
|
||||
.flow()
|
||||
.notes.stateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
val lastNote = channelState.channel.lastNote
|
||||
// The newest *timeline* message (not the raw lastNote): skips kind-1111 thread replies and
|
||||
// hidden authors so the preview + time match the channel feed and the unread badge below.
|
||||
val lastNote = remember(channelState) { channel.newestTimelineNote(account) }
|
||||
val unreadCount by
|
||||
remember(communityId, channelKey) { concordChannelUnreadCountFlow(account, communityId, channelKey) }
|
||||
.collectAsStateWithLifecycle(0)
|
||||
|
||||
+42
-3
@@ -20,11 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
@@ -51,12 +54,48 @@ fun concordChannelUnreadCountFlow(
|
||||
account.loadLastReadFlow(concordChannelLastReadRoute(communityId, channelKey)),
|
||||
channel.flow().notes.stateFlow,
|
||||
) { lastRead, _ ->
|
||||
channel.newMessagesSince(lastRead)
|
||||
channel.newMessagesSince(account, lastRead)
|
||||
}
|
||||
}
|
||||
|
||||
/** The number of this channel's messages created strictly after [sinceSecs] (0 if none). */
|
||||
private fun ConcordChannel.newMessagesSince(sinceSecs: Long): Int = notes.count { _, note -> (note.createdAt() ?: 0L) > sinceSecs }
|
||||
/**
|
||||
* True for a note the Concord channel *timeline* actually renders — the same predicate as
|
||||
* [com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedFilter]'s
|
||||
* `isTimelineMessage`: a loaded, acceptable message that is **not** a kind-1111 [CommentEvent].
|
||||
*
|
||||
* A [CommentEvent] is a *minichat thread reply* that lives inside its parent's thread, not on the
|
||||
* flat timeline, so it never composes on the channel screen and never advances the last-read
|
||||
* marker. Every list-row surface that summarizes a channel — the unread badge
|
||||
* ([newMessagesSince]), the last-message preview + timestamp ([newestTimelineNote]), and the
|
||||
* Messages hub row — reuses this so none of them can disagree with the open channel's feed:
|
||||
* a trailing comment can't stick the badge at a count the user can never clear, nor show up as a
|
||||
* "last message" that isn't in the timeline. Unacceptable (muted/blocked) authors are hidden for
|
||||
* the same reason.
|
||||
*/
|
||||
fun isConcordTimelineMessage(
|
||||
note: Note,
|
||||
account: Account,
|
||||
): Boolean = note.event.let { it != null && it !is CommentEvent } && account.isAcceptable(note)
|
||||
|
||||
/**
|
||||
* The newest timeline message in this channel (see [isConcordTimelineMessage]), or null if none —
|
||||
* the note the list/hub rows show as the channel's "last message". Unlike [ConcordChannel.lastNote]
|
||||
* (the raw newest note of any kind), this skips thread replies and hidden authors so the preview
|
||||
* matches what the channel feed renders and the unread badge counts.
|
||||
*/
|
||||
fun ConcordChannel.newestTimelineNote(account: Account): Note? =
|
||||
notes
|
||||
.filter { _, note -> isConcordTimelineMessage(note, account) }
|
||||
.minWithOrNull(Channel.DefaultFeedOrder)
|
||||
|
||||
/** The number of this channel's timeline messages created strictly after [sinceSecs] (0 if none). */
|
||||
private fun ConcordChannel.newMessagesSince(
|
||||
account: Account,
|
||||
sinceSecs: Long,
|
||||
): Int =
|
||||
notes.count { _, note ->
|
||||
(note.createdAt() ?: 0L) > sinceSecs && isConcordTimelineMessage(note, account)
|
||||
}
|
||||
|
||||
/**
|
||||
* The pubkeys of the [limit] most-recent distinct posters in this channel, newest first — the
|
||||
|
||||
+8
-2
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.isConcordTimelineMessage
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
@@ -400,10 +401,15 @@ class ChatroomListKnownFeedFilter(
|
||||
.sortedByDefaultFeedOrder()
|
||||
.firstOrNull()
|
||||
|
||||
/** The newest decrypted message loaded in this Concord channel, or null if none yet. */
|
||||
/**
|
||||
* The newest decrypted *timeline* message loaded in this Concord channel, or null if none yet.
|
||||
* Uses [isConcordTimelineMessage] so a trailing kind-1111 thread reply (or a hidden author)
|
||||
* isn't shown as the Messages-row "last message" — the same predicate the channel feed and the
|
||||
* unread badge use, so the row summary can't disagree with what opening the channel renders.
|
||||
*/
|
||||
private fun ConcordChannel.newestConcordNote(account: Account): Note? =
|
||||
notes
|
||||
.filter { _, it -> account.isAcceptable(it) && it.event != null }
|
||||
.filter { _, it -> isConcordTimelineMessage(it, account) }
|
||||
.sortedByDefaultFeedOrder()
|
||||
.firstOrNull()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user