diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index ac6a5e30a..1dfbda89f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -56,6 +56,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.dal.ShortsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.VideoFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.dal.WebBookmarkFeedFilter import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class AccountFeedContentStates( val account: Account, @@ -100,6 +102,19 @@ class AccountFeedContentStates( val webBookmarks = FeedContentState(WebBookmarkFeedFilter(account), scope, LocalCache) + init { + // Marmot group list changes (new group, group marked known, group + // metadata synced) don't flow through LocalCache.newEventBundles, so + // the additive update path can't see them. Force a full feed rebuild + // whenever the list changes so empty groups appear and placeholder + // rows get replaced by real messages. + scope.launch(Dispatchers.IO) { + account.marmotGroupList.groupListChanges.collect { + dmKnown.invalidateData() + } + } + } + suspend fun init() { notificationSummary.initializeSuspend() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 8e02c4945..cf2228a25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -85,7 +85,11 @@ fun ChatroomHeaderCompose( accountViewModel: AccountViewModel, nav: INav, ) { - if (baseNote.event != null) { + val isEmptyMarmotPlaceholder = + baseNote.event == null && + baseNote.inGatherers?.any { it is MarmotGroupChatroom } == true + + if (baseNote.event != null || isEmptyMarmotPlaceholder) { ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav) } else { val hasEvent by observeNoteHasEvent(baseNote, accountViewModel) @@ -248,21 +252,27 @@ private fun MarmotGroupRoomCompose( accountViewModel: AccountViewModel, nav: INav, ) { - val authorName by observeUserName(lastMessage.author!!, accountViewModel) val displayName by chatroom.displayName.collectAsStateWithLifecycle() val unread by chatroom.unreadCount.collectAsStateWithLifecycle() + val author = lastMessage.author val noteEvent = lastMessage.event - val description = noteEvent?.content?.take(200) - val groupName = displayName?.takeIf { it.isNotBlank() } ?: "Group ${chatroom.nostrGroupId.take(8)}" + val lastContent = + if (author != null && noteEvent != null) { + val authorName by observeUserName(author, accountViewModel) + "$authorName: ${noteEvent.content.take(200)}" + } else { + stringRes(R.string.marmot_group_no_messages_yet) + } + ChannelName( channelIdHex = chatroom.nostrGroupId, channelPicture = null, channelTitle = { modifier -> ChannelTitleWithLabelInfo(groupName, R.string.marmot_group, modifier) }, channelLastTime = lastMessage.createdAt(), - channelLastContent = "$authorName: $description", + channelLastContent = lastContent, hasNewMessages = unread > 0, loadProfilePicture = accountViewModel.settings.showProfilePictures(), loadRobohash = accountViewModel.settings.isNotPerformanceMode(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 932e3197f..e826250c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -79,7 +79,7 @@ class ChatroomListKnownFeedFilter( val marmotGroups = account.marmotGroupList.rooms.mapNotNull { _, chatroom -> - chatroom.newestMessage + chatroom.newestMessage ?: chatroom.placeholderNote() } return (privateMessages + publicChannels + ephemeralChats + marmotGroups).sortedWith(DefaultFeedOrder) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 510936a64..b537cbcf6 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -284,6 +284,7 @@ Public Chat MLS Group + No messages yet Public Chat Metadata Public chats are visible to everyone on Nostr and anyone can participate on them. They are great for open communities around specific topics. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt index 537976e56..85a857f7f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt @@ -69,6 +69,27 @@ class MarmotGroupChatroom( */ var ownerSentMessage: Boolean = false + // Synthetic note used by list views to represent the group when no + // messages have been received yet. Lazily created and kept stable so + // equality-based feed diffing treats it as the same row across refreshes. + private var cachedPlaceholder: Note? = null + + @Synchronized + fun placeholderNote(): Note { + val existing = cachedPlaceholder + if (existing != null) return existing + val created = + Note(placeholderIdHex(nostrGroupId)).apply { + addGatherer(this@MarmotGroupChatroom) + } + cachedPlaceholder = created + return created + } + + companion object { + fun placeholderIdHex(nostrGroupId: HexKey): HexKey = "marmot-empty-$nostrGroupId" + } + private var changesFlow: WeakReference>> = WeakReference(null) fun changesFlow(): MutableSharedFlow> {