mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
feat(nip29): one-tap join from invite links carrying a code
An invite link's `?code=` now flows all the way to the join: tapping a `wss://relay'id?code=…` card/link (or opening it as a deep link) opens the group and, because the tap is itself the opt-in, fires the kind-9021 join with that code once — no more re-typing it into the join dialog. Plain (code-less) group links still just open the group for viewing. - Route.RelayGroup gains inviteCode; threaded through AppNavigation → RelayGroupChatScreen → RelayGroupTopBar - RelayGroupTopBar auto-joins once when a code is present and we're not already a member (reuses the optimistic "requested" state) - RelayGroupCard / ClickableRelayGroupLink / uriToRoute carry the parsed code Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
@@ -337,5 +337,5 @@ private fun relayGroupDirectRoute(nip19: NAddress): Route? {
|
||||
*/
|
||||
private fun relayGroupInviteRoute(uri: String): Route? {
|
||||
val link = GroupInviteLink.parse(uri.removePrefix(NOSTR_URI_PREFIX)) ?: return null
|
||||
return Route.RelayGroup(link.groupId, link.relayUrl.url)
|
||||
return Route.RelayGroup(link.groupId, link.relayUrl.url, inviteCode = link.code)
|
||||
}
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ fun ClickableRelayGroupLink(
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
nav.nav(Route.RelayGroup(invite.groupId, invite.relayUrl.url))
|
||||
nav.nav(Route.RelayGroup(invite.groupId, invite.relayUrl.url, inviteCode = invite.code))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupPreviewSubscription
|
||||
@@ -78,13 +78,14 @@ fun RelayGroupCard(
|
||||
val groupId = remember(invite) { invite.toGroupId() }
|
||||
|
||||
LoadRelayGroupChannel(groupId, accountViewModel) { channel ->
|
||||
RelayGroupCardContent(channel, accountViewModel, nav)
|
||||
RelayGroupCardContent(channel, invite.code, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayGroupCardContent(
|
||||
baseChannel: RelayGroupChannel,
|
||||
inviteCode: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -111,7 +112,11 @@ private fun RelayGroupCardContent(
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
onClick = { nav.nav(routeFor(channel)) },
|
||||
onClick = {
|
||||
nav.nav(
|
||||
Route.RelayGroup(channel.groupId.id, channel.groupId.relayUrl.url, inviteCode = inviteCode),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
) {
|
||||
Row(
|
||||
|
||||
@@ -568,6 +568,7 @@ fun BuildNavigation(
|
||||
relayUrl = it.relayUrl,
|
||||
draftId = it.draftId,
|
||||
replyToId = it.replyTo,
|
||||
inviteCode = it.inviteCode,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
@@ -642,6 +642,9 @@ sealed class Route {
|
||||
val relayUrl: String,
|
||||
val draftId: HexKey? = null,
|
||||
val replyTo: HexKey? = null,
|
||||
// NIP-29 invite code from a `wss://relay'id?code=…` link — auto-joins a closed
|
||||
// group when the screen opens. Null for a plain (view-only) group link.
|
||||
val inviteCode: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class RelayGroupServer(
|
||||
|
||||
+2
-1
@@ -38,6 +38,7 @@ fun RelayGroupChatScreen(
|
||||
relayUrl: String,
|
||||
draftId: HexKey? = null,
|
||||
replyToId: HexKey? = null,
|
||||
inviteCode: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -50,7 +51,7 @@ fun RelayGroupChatScreen(
|
||||
isInvertedLayout = true,
|
||||
topBar = {
|
||||
LoadRelayGroupChannel(channelId, accountViewModel) {
|
||||
RelayGroupTopBar(it, accountViewModel, nav)
|
||||
RelayGroupTopBar(it, inviteCode, accountViewModel, nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
|
||||
+14
@@ -64,6 +64,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
@Composable
|
||||
fun RelayGroupTopBar(
|
||||
baseChannel: RelayGroupChannel,
|
||||
inviteCode: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -81,6 +82,19 @@ fun RelayGroupTopBar(
|
||||
LaunchedEffect(membership) {
|
||||
if (membership.isMember()) requested = false
|
||||
}
|
||||
|
||||
// Arrived via an invite link carrying a join code (`wss://relay'id?code=…`): the
|
||||
// user already opted in by tapping it, so fire the kind-9021 join once (with the
|
||||
// code) instead of making them re-enter it. Guarded to exactly once and skipped if
|
||||
// the roster already shows us as a member.
|
||||
var autoJoined by remember(channel.groupId) { mutableStateOf(false) }
|
||||
LaunchedEffect(channel.groupId, inviteCode, membership) {
|
||||
if (inviteCode != null && !autoJoined && !membership.isMember()) {
|
||||
autoJoined = true
|
||||
requested = true
|
||||
accountViewModel.joinRelayGroup(channel, inviteCode)
|
||||
}
|
||||
}
|
||||
val displayMembership = if (!membership.isMember() && requested) RelayGroupMembership.PENDING else membership
|
||||
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
|
||||
Reference in New Issue
Block a user