diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
index 286030e4f8..c15d31f6d6 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
@@ -1672,6 +1672,19 @@ class AccountViewModel(
fun leaveRelayGroup(channel: RelayGroupChannel) = launchSigner { account.leaveRelayGroup(channel) }
+ /**
+ * Take a relay group off Messages WITHOUT leaving it: drop it from my kind-10009 list so it stops
+ * showing, but send no kind-9022 — I stay in the relay roster and can still read/post, and re-joining
+ * re-surfaces it instantly. Also records it in `dismissedChannelInvites` so a Buzz relay re-announcing
+ * my membership (kind-44100) can't bounce it back in as a pending invite. This is the soft counterpart
+ * to [leaveRelayGroup]; "Remove from Messages" vs "Leave" is the same split the invite card offers.
+ */
+ fun removeRelayGroupFromMessages(channel: RelayGroupChannel) =
+ launchSigner {
+ account.settings.dismissChannelInvite(channel.groupId.id)
+ account.unfollow(channel)
+ }
+
/**
* Accept a channel somebody added me to: write it into my kind-10009 so it shows on Messages and
* follows me to other devices. No kind-9021 join — the relay already put me in the roster, which is
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt
index e555e14224..677bbec693 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt
@@ -205,7 +205,7 @@ private fun DmRowCard(
},
)
DropdownMenuItem(
- text = { Text(stringRes(R.string.buzz_dm_hide)) },
+ text = { Text(stringRes(R.string.remove_from_messages)) },
leadingIcon = {
Icon(
symbol = MaterialSymbols.VisibilityOff,
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt
index ffe49cce4d..55c7509ee8 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt
@@ -529,7 +529,7 @@ private fun rememberConcordDisplayName(
* so leaving is what actually retires the community for them.
*/
@Composable
-private fun ConcordLeaveDialog(
+internal fun ConcordLeaveDialog(
communityName: String,
isOwner: Boolean,
onDismiss: () -> Unit,
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt
index a4c9c159be..7cbb471afc 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt
@@ -113,6 +113,9 @@ fun RelayGroupTopBar(
val dmOther = if (isDm) channel.event?.buzzParticipants()?.firstOrNull { it != myPubkey } else null
var menuOpen by remember { mutableStateOf(false) }
+ // Read once here (nav.canPop() is @Composable) so the post-action navigation can pop from a menu
+ // callback — leaving/removing a group shouldn't strand the user on the screen of a group they left.
+ val canPop = nav.canPop()
var showInvite by remember { mutableStateOf(false) }
var showJoinCode by remember { mutableStateOf(false) }
@@ -298,11 +301,23 @@ fun RelayGroupTopBar(
},
)
}
+ // Two distinct actions, never conflated: "Remove from Messages" drops the group
+ // from my kind-10009 list but keeps my relay membership; "Leave" sends the
+ // kind-9022 that actually removes me. Same split as the channel-invite card.
DropdownMenuItem(
- text = { Text(stringRes(R.string.leave)) },
+ text = { Text(stringRes(R.string.remove_from_messages)) },
+ onClick = {
+ menuOpen = false
+ accountViewModel.removeRelayGroupFromMessages(channel)
+ if (canPop) nav.popBack()
+ },
+ )
+ DropdownMenuItem(
+ text = { Text(stringRes(R.string.leave), color = MaterialTheme.colorScheme.error) },
onClick = {
menuOpen = false
accountViewModel.leaveRelayGroup(channel)
+ if (canPop) nav.popBack()
},
)
}
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 30512735c9..9440d667d8 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
@@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberM
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.reportWarningContentDescription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordCommunityPill
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordLeaveDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.concordChannelLastReadRoute
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.concordCommunityHasUnreadFlow
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel
@@ -489,36 +490,61 @@ private fun RelayGroupRoomCompose(
// A placeholder row (no messages yet) has a null createdAt and never lights the dot.
val lastReadTime by accountViewModel.account.loadLastReadFlow(relayGroupChannelLastReadRoute(channel.groupId)).collectAsStateWithLifecycle()
- ChannelName(
- channelIdHex = channel.groupId.id,
- channelPicture = channelPicture,
- channelTitle = { modifier ->
- Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
- Text(
- text = channel.toBestDisplayName(),
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.weight(1f, fill = false),
- )
- Spacer(Modifier.width(6.dp))
- RelayNameChip(
- label = channel.groupId.relayUrl.displayUrl(),
- onClick = { nav.nav(Route.RelayGroupServer(channel.groupId.relayUrl.url)) },
- )
- }
- },
- channelLastTime = lastMessage.createdAt(),
- channelLastContent = lastContent,
- hasNewMessages = (lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime,
- loadProfilePicture = accountViewModel.settings.showProfilePictures(),
- loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
- autoPlayGif =
- accountViewModel.settings.autoPlayVideosFlow
- .collectAsStateWithLifecycle()
- .value,
- onClick = { nav.nav(Route.RelayGroup(channel.groupId.id, channel.groupId.relayUrl.url)) },
- )
+ // Long-press brings the group's membership actions to the Messages row itself, mirroring the group
+ // top bar so "Remove from Messages" (drop from my list, stay a member) and "Leave" (kind-9022) are
+ // reachable without opening the group first.
+ var menuOpen by remember { mutableStateOf(false) }
+
+ Box {
+ ChannelName(
+ channelIdHex = channel.groupId.id,
+ channelPicture = channelPicture,
+ channelTitle = { modifier ->
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
+ Text(
+ text = channel.toBestDisplayName(),
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.weight(1f, fill = false),
+ )
+ Spacer(Modifier.width(6.dp))
+ RelayNameChip(
+ label = channel.groupId.relayUrl.displayUrl(),
+ onClick = { nav.nav(Route.RelayGroupServer(channel.groupId.relayUrl.url)) },
+ )
+ }
+ },
+ channelLastTime = lastMessage.createdAt(),
+ channelLastContent = lastContent,
+ hasNewMessages = (lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime,
+ loadProfilePicture = accountViewModel.settings.showProfilePictures(),
+ loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
+ autoPlayGif =
+ accountViewModel.settings.autoPlayVideosFlow
+ .collectAsStateWithLifecycle()
+ .value,
+ onClick = { nav.nav(Route.RelayGroup(channel.groupId.id, channel.groupId.relayUrl.url)) },
+ onLongClick = { menuOpen = true },
+ )
+
+ DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
+ DropdownMenuItem(
+ text = { Text(stringRes(R.string.remove_from_messages)) },
+ onClick = {
+ menuOpen = false
+ accountViewModel.removeRelayGroupFromMessages(channel)
+ },
+ )
+ DropdownMenuItem(
+ text = { Text(stringRes(R.string.leave), color = MaterialTheme.colorScheme.error) },
+ onClick = {
+ menuOpen = false
+ accountViewModel.leaveRelayGroup(channel)
+ },
+ )
+ }
+ }
}
@Composable
@@ -550,40 +576,78 @@ private fun ConcordRoomCompose(
.loadLastReadFlow(concordChannelLastReadRoute(channel.channelId.communityId, channel.channelId.channelId))
.collectAsStateWithLifecycle()
- ChannelName(
- channelIdHex = channel.channelId.channelId,
- channelPicture = rememberConcordImageModel(channel.communityIcon, accountViewModel),
- channelTitle = { modifier ->
- Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
- Text(
- text = channel.toBestDisplayName(),
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.weight(1f, fill = false),
- )
- channel.communityName?.let { communityName ->
- Spacer(Modifier.width(6.dp))
- // The chip names the parent community and, when tapped, opens that community's
- // channel list — the "chip that opens the Concord Channel" entry point.
- ConcordCommunityPill(
- communityName = communityName,
- onClick = { nav.nav(Route.ConcordServer(channel.channelId.communityId)) },
+ // Concord has no server-side membership beyond my own kind-13302 list, so there is no soft
+ // "Remove from Messages" distinct from leaving — the only action is "Leave" (drop the community
+ // from my list = I'm out). Long-press surfaces it on the row with the same confirm the community
+ // screen uses; leaving a channel row leaves the whole community it belongs to (the dialog names it).
+ val communityId = channel.channelId.communityId
+ val isOwner =
+ accountViewModel.account.concordSessions
+ .sessionFor(communityId)
+ ?.entry
+ ?.owner == accountViewModel.account.signer.pubKey
+ var menuOpen by remember { mutableStateOf(false) }
+ var showLeave by remember { mutableStateOf(false) }
+
+ if (showLeave) {
+ ConcordLeaveDialog(
+ communityName = channel.communityName ?: channel.toBestDisplayName(),
+ isOwner = isOwner,
+ onDismiss = { showLeave = false },
+ onConfirm = {
+ showLeave = false
+ accountViewModel.leaveConcordCommunity(communityId)
+ },
+ )
+ }
+
+ Box {
+ ChannelName(
+ channelIdHex = channel.channelId.channelId,
+ channelPicture = rememberConcordImageModel(channel.communityIcon, accountViewModel),
+ channelTitle = { modifier ->
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
+ Text(
+ text = channel.toBestDisplayName(),
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.weight(1f, fill = false),
)
+ channel.communityName?.let { communityName ->
+ Spacer(Modifier.width(6.dp))
+ // The chip names the parent community and, when tapped, opens that community's
+ // channel list — the "chip that opens the Concord Channel" entry point.
+ ConcordCommunityPill(
+ communityName = communityName,
+ onClick = { nav.nav(Route.ConcordServer(channel.channelId.communityId)) },
+ )
+ }
}
- }
- },
- channelLastTime = lastMessage.createdAt(),
- channelLastContent = lastContent,
- hasNewMessages = (lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime,
- loadProfilePicture = accountViewModel.settings.showProfilePictures(),
- loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
- autoPlayGif =
- accountViewModel.settings.autoPlayVideosFlow
- .collectAsStateWithLifecycle()
- .value,
- onClick = { nav.nav(Route.Concord(channel.channelId.communityId, channel.channelId.channelId)) },
- )
+ },
+ channelLastTime = lastMessage.createdAt(),
+ channelLastContent = lastContent,
+ hasNewMessages = (lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime,
+ loadProfilePicture = accountViewModel.settings.showProfilePictures(),
+ loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
+ autoPlayGif =
+ accountViewModel.settings.autoPlayVideosFlow
+ .collectAsStateWithLifecycle()
+ .value,
+ onClick = { nav.nav(Route.Concord(channel.channelId.communityId, channel.channelId.channelId)) },
+ onLongClick = { menuOpen = true },
+ )
+
+ DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
+ DropdownMenuItem(
+ text = { Text(stringRes(R.string.leave), color = MaterialTheme.colorScheme.error) },
+ onClick = {
+ menuOpen = false
+ showLeave = true
+ },
+ )
+ }
+ }
}
@Composable
@@ -935,6 +999,7 @@ fun ChannelName(
loadRobohash: Boolean,
autoPlayGif: Boolean,
onClick: () -> Unit,
+ onLongClick: (() -> Unit)? = null,
) {
ChannelName(
channelPicture = {
@@ -953,6 +1018,7 @@ fun ChannelName(
channelLastContent,
hasNewMessages,
onClick,
+ onLongClick,
)
}
@@ -964,6 +1030,7 @@ fun ChannelName(
channelLastContent: String?,
hasNewMessages: Boolean,
onClick: () -> Unit,
+ onLongClick: (() -> Unit)? = null,
) {
ChatHeaderLayout(
channelPicture = channelPicture,
@@ -998,6 +1065,7 @@ fun ChannelName(
}
},
onClick = onClick,
+ onLongClick = onLongClick,
)
}
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index 04405d49b9..8530ecfd3d 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -448,6 +448,7 @@
Refresh
New chat profile:
Leave
+ Remove from Messages
Unfollow
Channel created
"Channel Information changed to"
@@ -3535,7 +3536,6 @@
No direct messages yet
Start a private conversation with anyone on a Buzz workspace.
More
- Hide conversation
Add member
Add someone to this DM
Not a valid npub or hex key