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> {