From 7ffc38f55d29ffa49b46754f8aaa1f7b00e52737 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 01:25:27 +0000 Subject: [PATCH] feat: promote Relay Groups to a top-level destination + live rosters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames the feature to "Relay Groups" and makes it a first-class navigation destination rather than a Messages-tab sub-view, and keeps joined-group membership accurate app-wide. - Top-level Route.RelayGroups home (RelayGroupsHomeScreen): a root destination with the drawer top bar + bottom bar, listing the host relays of joined groups (each drilling into its channels) plus the discovery entry. Registered as NavBarItem.RELAY_GROUPS (Forum icon) in the drawer's Feeds section and pinnable to the bottom bar, with a roster preloader entry. - Naming: user-facing strings now say "group(s)" instead of "channel(s)"; added relay_groups_title = "Relay Groups". - Live rosters: RelayGroupRosterFilterAssembler keeps every joined group's 39000/39001/39002 fresh (one #d-scoped filter per host relay, re-derived from the join list) while a groups-bearing screen is on top. Mounted on both Messages panes and the new home, so membership, pending→member transitions and member counts stay accurate without opening each chat — the gap that most affected closed/private groups. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj --- .../RelaySubscriptionsCoordinator.kt | 3 + .../amethyst/ui/navigation/AppNavigation.kt | 2 + .../ui/navigation/bottombars/NavBarItem.kt | 9 ++ .../amethyst/ui/navigation/routes/Routes.kt | 2 + .../loggedIn/BottomBarFeedPreloaders.kt | 3 + .../relayGroup/RelayGroupsHomeScreen.kt | 85 ++++++++++++++ .../RelayGroupRosterFilterAssembler.kt | 106 ++++++++++++++++++ .../RelayGroupRosterSubscription.kt | 45 ++++++++ .../rooms/singlepane/MessagesSinglePane.kt | 5 + .../chats/rooms/twopane/MessagesTwoPane.kt | 3 + amethyst/src/main/res/values/strings.xml | 27 ++--- 11 files changed, 277 insertions(+), 13 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupsHomeScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupRosterFilterAssembler.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupRosterSubscription.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt index fc25678611..7902367100 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.Calend import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource.ChatroomFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupDirectoryFilterAssembler +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupRosterFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssembler @@ -115,6 +116,7 @@ class RelaySubscriptionsCoordinator( // active depending on the screen. val channel = ChannelFilterAssembler(client) val relayGroupDirectory = RelayGroupDirectoryFilterAssembler(client) + val relayGroupRoster = RelayGroupRosterFilterAssembler(client) val chatroom = ChatroomFilterAssembler(client) val community = CommunityFilterAssembler(client) val gitRepository = RepositoryFilterAssembler(client) @@ -177,6 +179,7 @@ class RelaySubscriptionsCoordinator( val all = listOf( relayGroupDirectory, + relayGroupRoster, account, accountForeground, home, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index b7ab28a34f..44401ca49e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -109,6 +109,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayG import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupMembersScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupsHomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.share.ShareToDMScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen @@ -371,6 +372,7 @@ fun BuildNavigation( composableFromEnd { ProductsScreen(accountViewModel, nav) } composableFromEnd { ShortsScreen(accountViewModel, nav) } composableFromEnd { PublicChatsScreen(accountViewModel, nav) } + composableFromEnd { RelayGroupsHomeScreen(accountViewModel, nav) } composableFromEnd { FollowPacksScreen(accountViewModel, nav) } composableFromEnd { LiveStreamsScreen(accountViewModel, nav) } composableFromEnd { NestsScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt index aab1307013..aeeb9077c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt @@ -66,6 +66,7 @@ enum class NavBarItem { PODCAST_EPISODES, PODCASTS, PUBLIC_CHATS, + RELAY_GROUPS, FOLLOW_PACKS, LIVE_STREAMS, NESTS, @@ -318,6 +319,13 @@ val NavBarCatalog: Map = icon = MaterialSymbols.AutoMirrored.Chat, resolveRoute = { Route.PublicChats }, ), + NavBarItem.RELAY_GROUPS to + NavBarItemDef( + id = NavBarItem.RELAY_GROUPS, + labelRes = R.string.relay_groups_title, + icon = MaterialSymbols.Forum, + resolveRoute = { Route.RelayGroups }, + ), NavBarItem.FOLLOW_PACKS to NavBarItemDef( id = NavBarItem.FOLLOW_PACKS, @@ -439,6 +447,7 @@ val DrawerFeedsItems: List = NavBarItem.NESTS, NavBarItem.COMMUNITIES, NavBarItem.PUBLIC_CHATS, + NavBarItem.RELAY_GROUPS, NavBarItem.CALENDARS, NavBarItem.CALENDAR_COLLECTIONS, NavBarItem.SOFTWARE_APPS, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 4201f5b486..4554235a43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -653,6 +653,8 @@ sealed class Route { val relayUrl: String, ) : Route() + @Serializable object RelayGroups : Route() + @Serializable object RelayGroupBrowse : Route() @Serializable data class ChannelMetadataEdit( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt index 8052f08ab3..bc5becb547 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupRosterSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription @@ -131,6 +132,8 @@ private fun PreloadFor( NavBarItem.PUBLIC_CHATS -> PublicChatsFilterAssemblerSubscription(accountViewModel) + NavBarItem.RELAY_GROUPS -> RelayGroupRosterSubscription(accountViewModel.dataSources().relayGroupRoster, accountViewModel) + NavBarItem.FOLLOW_PACKS -> FollowPacksFilterAssemblerSubscription(accountViewModel) NavBarItem.LIVE_STREAMS -> LiveStreamsFilterAssemblerSubscription(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupsHomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupsHomeScreen.kt new file mode 100644 index 0000000000..315e022816 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupsHomeScreen.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold +import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupRosterSubscription +import com.vitorpamplona.amethyst.ui.stringRes + +/** + * The top-level "Relay Groups" destination — one tap from the nav drawer / bottom + * bar, distinct from the DM-centric Messages tab. Lists the host relays of the + * groups the user has joined (each drilling into that relay's channels) and offers + * the discovery entry to find more. Keeps every joined group's roster live while + * on top via [RelayGroupRosterSubscription]. + */ +@Composable +fun RelayGroupsHomeScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + RelayGroupRosterSubscription(accountViewModel.dataSources().relayGroupRoster, accountViewModel) + + DisappearingScaffold( + isInvertedLayout = false, + topBar = { + UserDrawerSearchTopBar(accountViewModel, nav) { + Text( + text = stringRes(R.string.relay_groups_title), + fontWeight = FontWeight.Bold, + ) + } + }, + bottomBar = { + AppBottomBar(Route.RelayGroups, nav, accountViewModel) { route -> + nav.navBottomBar(route) + } + }, + accountViewModel = accountViewModel, + ) { padding -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()), + ) { + // Joined relays (each drills into its channels), followed by the + // always-present "Find groups" discovery row. + RelayGroupServerList(accountViewModel, nav) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupRosterFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupRosterFilterAssembler.kt new file mode 100644 index 0000000000..1195adf960 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupRosterFilterAssembler.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource + +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent + +/** One screen's request to keep the roster of the user's joined groups fresh. */ +class RelayGroupRosterQueryState( + val account: Account, +) + +/** + * Roster kinds only (39000 metadata + 39001 admins + 39002 members) — enough to + * resolve name, member count and this user's membership. Roles (39003) are pulled + * by the per-chat / directory subscriptions when actually needed. + */ +private val RELAY_GROUP_ROSTER_KINDS = + listOf( + GroupMetadataEvent.KIND, + GroupAdminsEvent.KIND, + GroupMembersEvent.KIND, + ) + +/** + * Keeps the relay-signed roster (metadata/admins/members) of every group the user + * has joined live while a groups-bearing screen is on top, so membership, + * pending→member transitions and member counts stay accurate in list views + * without having to open each chat. This matters most for closed/private groups, + * where the only way to confirm a join was admitted is a fresh 39002. + * + * One subscription per host relay, scoped by `#d` to just that relay's joined + * group ids — so we don't pull a relay's whole directory, only what we're in. + */ +class RelayGroupRosterFilterAssembler( + client: INostrClient, +) : ComposeSubscriptionManager() { + val group = + listOf( + RelayGroupRosterSubAssembler(client, ::allKeys), + ) + + override fun invalidateKeys() = invalidateFilters() + + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } + + override fun destroy() = group.forEach { it.destroy() } +} + +class RelayGroupRosterSubAssembler( + client: INostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: RelayGroupRosterQueryState, + since: SincePerRelayMap?, + ): List? { + val joined = key.account.relayGroupList.liveRelayGroupList.value + if (joined.isEmpty()) return null + + // Group the joined group ids by their host relay: one #d-scoped filter each. + val idsByRelay = joined.groupBy({ it.relayUrl }, { it.groupId }) + + return idsByRelay.mapNotNull { (relayUrl, groupIds) -> + val relay = RelayUrlNormalizer.normalizeOrNull(relayUrl) ?: return@mapNotNull null + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = RELAY_GROUP_ROSTER_KINDS, + tags = mapOf("d" to groupIds.distinct()), + since = since?.get(relay)?.time, + ), + ) + } + } + + override fun id(key: RelayGroupRosterQueryState) = key.account +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupRosterSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupRosterSubscription.kt new file mode 100644 index 0000000000..8b747bd0c2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupRosterSubscription.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +/** + * Mount on any screen that lists the user's joined groups (the Messages tab's + * inline/grouped views, the Relay Groups home) to keep their rosters live. The + * assembler re-derives its per-relay filters from the joined-group list, so it + * follows joins/leaves automatically. + */ +@Composable +fun RelayGroupRosterSubscription( + dataSource: RelayGroupRosterFilterAssembler, + accountViewModel: AccountViewModel, +) { + val state = + remember(accountViewModel.account) { + RelayGroupRosterQueryState(accountViewModel.account) + } + + LifecycleAwareKeyDataSourceSubscription(state, dataSource) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt index 36f16f387d..3409f6db6a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupServerList import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupViewModeToggle +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupRosterSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelFabColumn import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.MessagesPager @@ -106,6 +107,10 @@ fun MessagesSinglePane( val viewMode by accountViewModel.account.settings.relayGroupViewMode .collectAsStateWithLifecycle() + // Keep joined groups' rosters live while the messages list is on top, so + // membership/pending state is accurate inline without opening each chat. + RelayGroupRosterSubscription(accountViewModel.dataSources().relayGroupRoster, accountViewModel) + Column { RelayGroupViewModeToggle(accountViewModel) if (viewMode == RelayGroupViewMode.GROUPED) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 7be553bb1d..d40f9f0a04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomVie import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupServerList import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupViewModeToggle +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupRosterSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelFabColumn import com.vitorpamplona.amethyst.ui.theme.Size20dp @@ -102,6 +103,8 @@ fun MessagesTwoPane( val viewMode by accountViewModel.account.settings.relayGroupViewMode .collectAsStateWithLifecycle() + RelayGroupRosterSubscription(accountViewModel.dataSources().relayGroupRoster, accountViewModel) + Column(Modifier.fillMaxSize()) { RelayGroupViewModeToggle(accountViewModel) if (viewMode == RelayGroupViewMode.GROUPED) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index eff9d71e78..648650f782 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1929,22 +1929,23 @@ Public Group New Public or Private Group + Relay Groups Inline By relay - Create a channel - Channel name + Create a group + Group name Topic (optional) Private (members-only read) Invite only Create Invite people - Share this code so people can join this channel. + Share this code so people can join this group. Creating an invite… Invite code copied - Invite code (for this invite-only channel): - Join channel + Invite code (for this invite-only group): + Join group Invite code - This channel is invite-only. Enter the code you were given. + This group is invite-only. Enter the code you were given. Join Requested Admin @@ -1954,18 +1955,18 @@ Make admin Make moderator Remove role - Remove from channel - Remove %1$s from this channel? They will lose access until re-added or re-invited. - Edit channel - Channel name + Remove from group + Remove %1$s from this group? They will lose access until re-added or re-invited. + Edit group + Group name Topic (optional) Private (members-only read) Invite only Save Members - Edit channel - Find channels - Browse the channels hosted on a group relay. Paste a relay address, or pick one below. + Edit group + Find groups + Browse the groups hosted on a relay. Paste a relay address, or pick one below. Relay address Browse Relays you\'re on