mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
feat: align leave vs remove-from-messages actions across chat types
"Leave" meant three different things and the actions were scattered across each channel's own screen, so they were hard to find from Messages. Make the vocabulary consistent and reachable: - "Leave" now always means "renounce membership / you're out" — the kind-9022 LeaveRequestEvent for NIP-29/Buzz groups, and the kind-13302 self-list removal for Concord (its only exit). - "Remove from Messages" is the single soft action: take it off my list but keep membership. For a joined relay group this drops the kind-10009 entry without a 9022 (I stay in the roster) and dismisses the invite so a Buzz kind-44100 re-announce can't bounce it back. The Buzz DM "Hide conversation" reuses the same label. Surface both on the Messages rows via long-press (previously only reachable inside each group/community screen): - Relay-group row: "Remove from Messages" + "Leave". - Concord row: "Leave" (reuses the existing confirm dialog; a community has no soft/hard split since the list entry is the whole membership). Split the relay-group top-bar menu into the same two actions, thread an optional onLongClick through ChannelName, and consolidate the buzz_dm_hide string into the shared remove_from_messages string. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NRifAJ75U4zWbg3V3Y3g8g
This commit is contained in:
+13
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+16
-1
@@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+130
-62
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -448,6 +448,7 @@
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="changed_chat_profile_to">New chat profile:</string>
|
||||
<string name="leave">Leave</string>
|
||||
<string name="remove_from_messages">Remove from Messages</string>
|
||||
<string name="unfollow">Unfollow</string>
|
||||
<string name="channel_created">Channel created</string>
|
||||
<string name="channel_information_changed_to">"Channel Information changed to"</string>
|
||||
@@ -3535,7 +3536,6 @@
|
||||
<string name="buzz_dm_empty_title">No direct messages yet</string>
|
||||
<string name="buzz_dm_empty_body">Start a private conversation with anyone on a Buzz workspace.</string>
|
||||
<string name="buzz_dm_more">More</string>
|
||||
<string name="buzz_dm_hide">Hide conversation</string>
|
||||
<string name="buzz_dm_add_member">Add member</string>
|
||||
<string name="buzz_dm_add_member_title">Add someone to this DM</string>
|
||||
<string name="buzz_dm_add_member_invalid">Not a valid npub or hex key</string>
|
||||
|
||||
Reference in New Issue
Block a user