mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 11:48:24 +00:00
feat(nip29): join / leave / create / invite actions
Wire the NIP-29 membership + admin flows into the UI, all published only to the group's host relay: - Account: joinRelayGroup (9021 + follow), leaveRelayGroup (9022 + unfollow), createRelayGroup (9007 + 9002, returns the new GroupId), createRelayGroupInvite (9009); AccountViewModel wrappers. - RelayGroupTopBar: a Join button when not a member; once joined, an overflow with Invite + Leave. Membership is read from the kind-10009 list. - CreateRelayGroupDialog: name/topic/private/invite-only; mints a random group id, publishes, and navigates into the new channel. Reachable from a FAB on the relay channel-list screen. - InviteRelayGroupDialog: mints a kind-9009 invite code on open and offers copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
@@ -202,6 +202,13 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent
|
||||
import com.vitorpamplona.quartz.nip32Labeling.LabelEvent
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache
|
||||
@@ -1455,6 +1462,65 @@ class Account(
|
||||
|
||||
suspend fun unfollow(channel: RelayGroupChannel) = sendMyPublicAndPrivateOutbox(relayGroupList.unfollow(channel))
|
||||
|
||||
// ── NIP-29 relay-group actions ───────────────────────────────────────────
|
||||
// All group commands are published ONLY to the group's host relay, where
|
||||
// relay29 authorizes them. The relay is the source of truth; the kind-10009
|
||||
// list is our own cross-device bookkeeping of what we joined.
|
||||
|
||||
/** Send a kind 9021 join request to the group's host relay and remember it. */
|
||||
suspend fun joinRelayGroup(
|
||||
channel: RelayGroupChannel,
|
||||
code: String? = null,
|
||||
) {
|
||||
val template = JoinRequestEvent.build(channel.groupId.id, inviteCode = code)
|
||||
signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
|
||||
follow(channel)
|
||||
}
|
||||
|
||||
/** Send a kind 9022 leave request to the host relay and drop it from our list. */
|
||||
suspend fun leaveRelayGroup(channel: RelayGroupChannel) {
|
||||
val template = LeaveRequestEvent.build(channel.groupId.id)
|
||||
signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
|
||||
unfollow(channel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new group on [relay]: kind 9007 (create-group) then kind 9002
|
||||
* (edit-metadata) with the chosen name/visibility, then remember it. Returns
|
||||
* the new group's id.
|
||||
*/
|
||||
suspend fun createRelayGroup(
|
||||
relay: NormalizedRelayUrl,
|
||||
groupId: String,
|
||||
name: String,
|
||||
about: String? = null,
|
||||
isPrivate: Boolean = false,
|
||||
isClosed: Boolean = false,
|
||||
): GroupId {
|
||||
signAndSendPrivatelyOrBroadcast(CreateGroupEvent.build(groupId)) { listOf(relay) }
|
||||
|
||||
val status =
|
||||
buildSet {
|
||||
add(if (isPrivate) GroupMetadataEvent.GroupStatus.PRIVATE else GroupMetadataEvent.GroupStatus.PUBLIC)
|
||||
add(if (isClosed) GroupMetadataEvent.GroupStatus.CLOSED else GroupMetadataEvent.GroupStatus.OPEN)
|
||||
}
|
||||
val edit = EditMetadataEvent.build(groupId, name = name, about = about, status = status)
|
||||
signAndSendPrivatelyOrBroadcast(edit) { listOf(relay) }
|
||||
|
||||
val id = GroupId(groupId, relay)
|
||||
follow(LocalCache.getOrCreateRelayGroupChannel(id))
|
||||
return id
|
||||
}
|
||||
|
||||
/** Mint a kind 9009 invite code for the group (admin/moderator only). */
|
||||
suspend fun createRelayGroupInvite(
|
||||
channel: RelayGroupChannel,
|
||||
code: String,
|
||||
) {
|
||||
val template = CreateInviteEvent.build(channel.groupId.id, code)
|
||||
signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
|
||||
}
|
||||
|
||||
suspend fun follow(community: AddressableNote) = sendMyPublicAndPrivateOutbox(communityList.follow(community))
|
||||
|
||||
suspend fun unfollow(community: AddressableNote) = sendMyPublicAndPrivateOutbox(communityList.unfollow(community))
|
||||
|
||||
+21
@@ -1431,6 +1431,27 @@ class AccountViewModel(
|
||||
|
||||
fun unfollow(channel: EphemeralChatChannel) = launchSigner { account.unfollow(channel) }
|
||||
|
||||
fun joinRelayGroup(
|
||||
channel: RelayGroupChannel,
|
||||
code: String? = null,
|
||||
) = launchSigner { account.joinRelayGroup(channel, code) }
|
||||
|
||||
fun leaveRelayGroup(channel: RelayGroupChannel) = launchSigner { account.leaveRelayGroup(channel) }
|
||||
|
||||
fun createRelayGroup(
|
||||
relay: NormalizedRelayUrl,
|
||||
groupId: String,
|
||||
name: String,
|
||||
about: String?,
|
||||
isPrivate: Boolean,
|
||||
isClosed: Boolean,
|
||||
) = launchSigner { account.createRelayGroup(relay, groupId, name, about, isPrivate, isClosed) }
|
||||
|
||||
fun createRelayGroupInvite(
|
||||
channel: RelayGroupChannel,
|
||||
code: String,
|
||||
) = launchSigner { account.createRelayGroupInvite(channel, code) }
|
||||
|
||||
fun follow(users: List<User>) = launchSigner { account.follow(users) }
|
||||
|
||||
fun follow(user: User) = launchSigner { account.follow(user) }
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
|
||||
/** Random NIP-29 group id: 8 secure bytes, hex-encoded (matches Armada). */
|
||||
private fun randomGroupId(): String = RandomInstance.bytes(8).toHexKey()
|
||||
|
||||
/**
|
||||
* Create a NIP-29 group on [relay]: publishes kind 9007 + 9002 with the chosen
|
||||
* name/visibility, then navigates into the new group. The user becomes its first
|
||||
* admin.
|
||||
*/
|
||||
@Composable
|
||||
fun CreateRelayGroupDialog(
|
||||
relay: NormalizedRelayUrl,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var about by remember { mutableStateOf("") }
|
||||
var isPrivate by remember { mutableStateOf(false) }
|
||||
var isClosed by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringRes(R.string.relay_group_create_title)) },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringRes(R.string.relay_group_create_name)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = about,
|
||||
onValueChange = { about = it },
|
||||
label = { Text(stringRes(R.string.relay_group_create_topic)) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
ToggleRow(stringRes(R.string.relay_group_create_private), isPrivate) { isPrivate = it }
|
||||
ToggleRow(stringRes(R.string.relay_group_create_invite_only), isClosed) { isClosed = it }
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = name.isNotBlank(),
|
||||
onClick = {
|
||||
val groupId = randomGroupId()
|
||||
accountViewModel.createRelayGroup(
|
||||
relay = relay,
|
||||
groupId = groupId,
|
||||
name = name.trim(),
|
||||
about = about.trim().ifBlank { null },
|
||||
isPrivate = isPrivate,
|
||||
isClosed = isClosed,
|
||||
)
|
||||
onDismiss()
|
||||
nav.nav(Route.RelayGroup(groupId, relay.url))
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.relay_group_create_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringRes(R.string.cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleRow(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
onChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(label, modifier = Modifier.weight(1f))
|
||||
Switch(checked = checked, onCheckedChange = onChange)
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
|
||||
/**
|
||||
* Mint a NIP-29 invite (kind 9009) for the group as soon as the dialog opens and
|
||||
* show the code to copy and share. Requires admin/moderator rights on the relay.
|
||||
*/
|
||||
@Composable
|
||||
fun InviteRelayGroupDialog(
|
||||
channel: RelayGroupChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val code by remember { mutableStateOf(RandomInstance.bytes(6).toHexKey()) }
|
||||
|
||||
// Publishing the invite is a one-shot side effect when the dialog opens.
|
||||
LaunchedEffect(code) {
|
||||
accountViewModel.createRelayGroupInvite(channel, code)
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringRes(R.string.relay_group_invite_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringRes(R.string.relay_group_invite_description))
|
||||
Text(
|
||||
text = code,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
clipboard.setText(AnnotatedString(code))
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringRes(R.string.copy))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringRes(R.string.cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
+14
@@ -26,14 +26,17 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -62,6 +65,8 @@ fun RelayGroupChannelListScreen(
|
||||
) {
|
||||
val relay = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } ?: return
|
||||
|
||||
var showCreate by remember { mutableStateOf(false) }
|
||||
|
||||
RelayGroupDirectorySubscription(relay, accountViewModel.dataSources().relayGroupDirectory, accountViewModel)
|
||||
|
||||
// The channel set grows as directory events arrive; poll the cache while the
|
||||
@@ -90,6 +95,11 @@ fun RelayGroupChannelListScreen(
|
||||
popBack = nav::popBack,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { showCreate = true }) {
|
||||
Text("+", style = MaterialTheme.typography.headlineMedium)
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
LazyColumn(modifier = Modifier.padding(padding)) {
|
||||
items(channels, key = { it.groupId.id }) { channel ->
|
||||
@@ -98,6 +108,10 @@ fun RelayGroupChannelListScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showCreate) {
|
||||
CreateRelayGroupDialog(relay, accountViewModel, nav) { showCreate = false }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+53
@@ -21,17 +21,28 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
|
||||
@Composable
|
||||
fun RelayGroupTopBar(
|
||||
@@ -43,6 +54,17 @@ fun RelayGroupTopBar(
|
||||
val channelState by observeChannel(baseChannel, accountViewModel)
|
||||
val channel = channelState?.channel as? RelayGroupChannel ?: baseChannel
|
||||
|
||||
val joinedGroups by accountViewModel.account.relayGroupList.liveRelayGroupList
|
||||
.collectAsStateWithLifecycle()
|
||||
val isJoined =
|
||||
joinedGroups.any {
|
||||
it.groupId == channel.groupId.id &&
|
||||
RelayUrlNormalizer.normalizeOrNull(it.relayUrl) == channel.groupId.relayUrl
|
||||
}
|
||||
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
var showInvite by remember { mutableStateOf(false) }
|
||||
|
||||
TopBarExtensibleWithBackButton(
|
||||
title = {
|
||||
Column {
|
||||
@@ -61,6 +83,37 @@ fun RelayGroupTopBar(
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (!isJoined) {
|
||||
FilledTonalButton(onClick = { accountViewModel.joinRelayGroup(channel) }) {
|
||||
Text(stringRes(R.string.join))
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = { menuOpen = true }) {
|
||||
Text("⋮", style = MaterialTheme.typography.titleLarge)
|
||||
}
|
||||
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.relay_group_invite_title)) },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
showInvite = true
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.leave)) },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
accountViewModel.leaveRelayGroup(channel)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
popBack = nav::popBack,
|
||||
)
|
||||
|
||||
if (showInvite) {
|
||||
InviteRelayGroupDialog(channel, accountViewModel) { showInvite = false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1931,6 +1931,16 @@
|
||||
<string name="messages_create_public_private_chat_description">New Public or Private Group</string>
|
||||
<string name="relay_group_view_inline">Inline</string>
|
||||
<string name="relay_group_view_grouped">By relay</string>
|
||||
<string name="relay_group_create_title">Create a channel</string>
|
||||
<string name="relay_group_create_name">Channel name</string>
|
||||
<string name="relay_group_create_topic">Topic (optional)</string>
|
||||
<string name="relay_group_create_private">Private (members-only read)</string>
|
||||
<string name="relay_group_create_invite_only">Invite only</string>
|
||||
<string name="relay_group_create_confirm">Create</string>
|
||||
<string name="relay_group_invite_title">Invite people</string>
|
||||
<string name="relay_group_invite_description">Share this code so people can join this channel.</string>
|
||||
<string name="relay_group_invite_generating">Creating an invite…</string>
|
||||
<string name="relay_group_invite_copied">Invite code copied</string>
|
||||
<string name="messages_new_message">Private</string>
|
||||
<string name="messages_new_message_to">To</string>
|
||||
<string name="messages_new_message_subject">Subject</string>
|
||||
|
||||
Reference in New Issue
Block a user