mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
fix: persist MLS chat unread state across app restarts
The unread dot for Marmot/MLS group rooms was driven by an in-memory unreadCount on MarmotGroupChatroom. On restart the MLS group state is restored from the last persisted commit, the kind:445 subscription restarts with since=null, and the in-memory processed-event dedup set is empty — so relays redeliver old group events, they re-decrypt as fresh application messages, and the counter was re-bumped, resurrecting the dot for chats already read. Marking-as-read was already persisted: opening a group chat writes the newest rendered message's createdAt to the MarmotGroup/<groupId> route in lastReadPerRoute (saved to disk with account settings). Compute the unread indicators from that timestamp instead — exactly how DM rooms and public channels do it — in both the Messages screen row and the Marmot group list row. With no consumer left, drop the volatile counter and collapse the addMessageSync/restoreMessageSync split (they only differed in the counter bump).
This commit is contained in:
@@ -3549,7 +3549,7 @@ class Account(
|
||||
if (isNew) {
|
||||
innerNote.event = innerEvent
|
||||
}
|
||||
marmotGroupList.restoreMessage(groupId, innerNote)
|
||||
marmotGroupList.addMessage(groupId, innerNote)
|
||||
} catch (e: Exception) {
|
||||
Log.w(
|
||||
"Account",
|
||||
|
||||
-5
@@ -100,11 +100,6 @@ fun MarmotGroupChatView(
|
||||
newMessageModel.init(accountViewModel)
|
||||
newMessageModel.load(nostrGroupId)
|
||||
|
||||
DisposableEffect(nostrGroupId) {
|
||||
newMessageModel.chatroom?.markAsRead()
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
// Resolve the navigation-supplied replyId (e.g. tapping reply on an MLS
|
||||
// message in the Notifications screen) into the actual Note once it has
|
||||
// landed in LocalCache. checkGetOrCreateNote is a no-op for unknown ids.
|
||||
|
||||
+6
-1
@@ -222,11 +222,16 @@ fun MarmotGroupListItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val displayName by chatroom.displayName.collectAsStateWithLifecycle()
|
||||
val unread by chatroom.unreadCount.collectAsStateWithLifecycle()
|
||||
val members by chatroom.members.collectAsStateWithLifecycle()
|
||||
val memberPubkeys = remember(members) { members.map { it.pubkey } }
|
||||
val newestMessage = chatroom.newestMessage
|
||||
|
||||
val lastReadTime by accountViewModel.account.loadLastReadFlow("MarmotGroup/$groupId").collectAsStateWithLifecycle()
|
||||
val unread =
|
||||
remember(newestMessage, lastReadTime) {
|
||||
chatroom.messages.count { (it.createdAt() ?: Long.MIN_VALUE) > lastReadTime }
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
|
||||
+3
-2
@@ -260,7 +260,6 @@ private fun MarmotGroupRoomCompose(
|
||||
nav: INav,
|
||||
) {
|
||||
val displayName by chatroom.displayName.collectAsStateWithLifecycle()
|
||||
val unread by chatroom.unreadCount.collectAsStateWithLifecycle()
|
||||
|
||||
val author = lastMessage.author
|
||||
val noteEvent = lastMessage.event
|
||||
@@ -274,13 +273,15 @@ private fun MarmotGroupRoomCompose(
|
||||
stringRes(R.string.marmot_group_no_messages_yet)
|
||||
}
|
||||
|
||||
val lastReadTime by accountViewModel.account.loadLastReadFlow("MarmotGroup/${chatroom.nostrGroupId}").collectAsStateWithLifecycle()
|
||||
|
||||
ChannelName(
|
||||
channelIdHex = chatroom.nostrGroupId,
|
||||
channelPicture = null,
|
||||
channelTitle = { modifier -> ChannelTitleWithLabelInfo(groupName, R.string.marmot_group, modifier) },
|
||||
channelLastTime = lastMessage.createdAt(),
|
||||
channelLastContent = lastContent,
|
||||
hasNewMessages = unread > 0,
|
||||
hasNewMessages = (lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
|
||||
-30
@@ -53,7 +53,6 @@ class MarmotGroupChatroom(
|
||||
var memberCount = MutableStateFlow(0)
|
||||
var members = MutableStateFlow<List<GroupMemberInfo>>(emptyList())
|
||||
var newestMessage: Note? = null
|
||||
val unreadCount = MutableStateFlow(0)
|
||||
|
||||
/**
|
||||
* Tracks the most recent createdAt (seconds) of a kind:445 group event
|
||||
@@ -125,30 +124,6 @@ class MarmotGroupChatroom(
|
||||
}
|
||||
|
||||
fun addMessageSync(msg: Note): Boolean =
|
||||
syncLock.withLock {
|
||||
if (msg !in messages) {
|
||||
messages = messages + msg
|
||||
msg.addGatherer(this)
|
||||
|
||||
val createdAt = msg.createdAt() ?: 0L
|
||||
if (createdAt > (newestMessage?.createdAt() ?: 0L)) {
|
||||
newestMessage = msg
|
||||
}
|
||||
|
||||
unreadCount.value += 1
|
||||
changesFlow?.get()?.tryEmit(ListChange.Addition(msg))
|
||||
return@withLock true
|
||||
}
|
||||
return@withLock false
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message that is being restored from persistent storage on app
|
||||
* startup. Behaves like [addMessageSync] but does NOT bump the unread
|
||||
* count — restored messages were already seen by the user in a previous
|
||||
* session.
|
||||
*/
|
||||
fun restoreMessageSync(msg: Note): Boolean =
|
||||
syncLock.withLock {
|
||||
if (msg !in messages) {
|
||||
messages = messages + msg
|
||||
@@ -181,10 +156,6 @@ class MarmotGroupChatroom(
|
||||
return@withLock false
|
||||
}
|
||||
|
||||
fun markAsRead() {
|
||||
unreadCount.value = 0
|
||||
}
|
||||
|
||||
fun recordRelayActivity(
|
||||
relay: NormalizedRelayUrl,
|
||||
createdAt: Long,
|
||||
@@ -220,7 +191,6 @@ class MarmotGroupChatroom(
|
||||
if (toRemove.isEmpty()) return@withLock toRemove
|
||||
messages = emptySet()
|
||||
newestMessage = null
|
||||
unreadCount.value = 0
|
||||
changesFlow?.get()?.tryEmit(ListChange.SetDeletion<Note>(toRemove))
|
||||
toRemove
|
||||
}
|
||||
|
||||
+1
-29
@@ -49,35 +49,7 @@ class MarmotGroupList(
|
||||
) {
|
||||
if (!isDisplayableFeedMessage(msg)) return
|
||||
val chatroom = getOrCreateGroup(nostrGroupId)
|
||||
val isSelfAuthored = msg.author?.pubkeyHex == ownerPubKey
|
||||
// Use the quiet path for our own messages so the relay round-trip
|
||||
// doesn't mark the user's own outgoing message as unread.
|
||||
val added =
|
||||
if (isSelfAuthored) {
|
||||
chatroom.restoreMessageSync(msg)
|
||||
} else {
|
||||
chatroom.addMessageSync(msg)
|
||||
}
|
||||
if (added) {
|
||||
noteToGroupIndex.getOrCreate(msg.idHex) { nostrGroupId }
|
||||
if (isSelfAuthored) {
|
||||
chatroom.ownerSentMessage = true
|
||||
}
|
||||
_groupListChanges.tryEmit(nostrGroupId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message that was restored from persistent storage at app startup.
|
||||
* Does not bump the chatroom's unread counter.
|
||||
*/
|
||||
fun restoreMessage(
|
||||
nostrGroupId: HexKey,
|
||||
msg: Note,
|
||||
) {
|
||||
if (!isDisplayableFeedMessage(msg)) return
|
||||
val chatroom = getOrCreateGroup(nostrGroupId)
|
||||
if (chatroom.restoreMessageSync(msg)) {
|
||||
if (chatroom.addMessageSync(msg)) {
|
||||
noteToGroupIndex.getOrCreate(msg.idHex) { nostrGroupId }
|
||||
if (msg.author?.pubkeyHex == ownerPubKey) {
|
||||
chatroom.ownerSentMessage = true
|
||||
|
||||
Reference in New Issue
Block a user