Merge pull request #2460 from vitorpamplona/claude/fix-empty-groups-display-MO6E9

Show placeholder notes for empty Marmot groups in chat list
This commit is contained in:
Vitor Pamplona
2026-04-20 13:15:26 -04:00
committed by GitHub
5 changed files with 53 additions and 6 deletions
@@ -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()
}
@@ -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(),
@@ -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)
+1
View File
@@ -284,6 +284,7 @@
<string name="public_chat">Public Chat</string>
<string name="marmot_group">MLS Group</string>
<string name="marmot_group_no_messages_yet">No messages yet</string>
<string name="public_chat_title">Public Chat Metadata</string>
<string name="public_chat_explainer">Public chats are visible to everyone on Nostr and anyone
can participate on them. They are great for open communities around specific topics.
@@ -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<MutableSharedFlow<ListChange<Note>>> = WeakReference(null)
fun changesFlow(): MutableSharedFlow<ListChange<Note>> {