feat(concord): create screen uses the relay picker + icon field

Replaces the comma-separated relay text box with the app's RelayUrlEditField
(type a relay → NIP-11 name/icon autocomplete, tap to add), shown above a
removable list of chosen relays. Adds an optional community icon URL, plumbed
through createConcordCommunity → ConcordActions.createCommunity →
ConcordCommunityFactory into MetadataEntity.icon. Section header styling matches
RelayGroupMetadataScreen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
Claude
2026-07-10 23:49:04 +00:00
parent bfc77e80f6
commit f7ee2964db
4 changed files with 60 additions and 12 deletions
@@ -1530,10 +1530,11 @@ class Account(
name: String,
description: String? = null,
relays: List<String> = emptyList(),
icon: String? = null,
): String? {
if (!isWriteable()) return null
val relayUrls = relays.ifEmpty { outboxRelays.flow.value.map { it.url } }
val community = ConcordActions.createCommunity(signer, name, TimeUtils.now(), description, relayUrls)
val community = ConcordActions.createCommunity(signer, name, TimeUtils.now(), description, relayUrls, icon)
val publishTo = relayUrls.mapNotNullTo(mutableSetOf()) { RelayUrlNormalizer.normalizeOrNull(it) }.ifEmpty { outboxRelays.flow.value }
community.genesisWraps.forEach { client.publish(it, publishTo) }
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.conco
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -30,8 +31,10 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
@@ -40,6 +43,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -47,13 +52,16 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
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.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import kotlinx.coroutines.launch
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon
/**
* Create a new Concord Channel (encrypted community) from Amethyst. Mints the
* genesis (metadata + #general), publishes it to the given relays (or the
* genesis (metadata + #general), publishes it to the chosen relays (or the
* account's outbox by default), joins it, and opens the new community.
*/
@OptIn(ExperimentalMaterial3Api::class)
@@ -64,7 +72,8 @@ fun ConcordCreateScreen(
) {
var name by remember { mutableStateOf("") }
var about by remember { mutableStateOf("") }
var relaysCsv by remember { mutableStateOf("") }
var iconUrl by remember { mutableStateOf("") }
val relays = remember { mutableListOf<NormalizedRelayUrl>().toMutableStateList() }
var working by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
@@ -103,28 +112,64 @@ fun ConcordCreateScreen(
label = { Text(stringRes(com.vitorpamplona.amethyst.R.string.concord_create_about)) },
)
OutlinedTextField(
value = relaysCsv,
onValueChange = { relaysCsv = it },
value = iconUrl,
onValueChange = { iconUrl = it },
modifier = Modifier.fillMaxWidth(),
label = { Text(stringRes(com.vitorpamplona.amethyst.R.string.concord_create_relays)) },
placeholder = { Text("wss://relay.one, wss://relay.two") },
singleLine = true,
label = { Text(stringRes(com.vitorpamplona.amethyst.R.string.concord_create_icon)) },
placeholder = { Text("https://…/icon.png") },
)
SectionHeader(stringRes(com.vitorpamplona.amethyst.R.string.concord_create_relays))
relays.forEach { relay ->
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(relay.displayUrl(), Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium)
IconButton(onClick = { relays.remove(relay) }) {
SymbolIcon(symbol = MaterialSymbols.Close, contentDescription = stringRes(com.vitorpamplona.amethyst.R.string.remove))
}
}
}
RelayUrlEditField(
onNewRelay = { if (it !in relays) relays.add(it) },
modifier = Modifier.fillMaxWidth(),
accountViewModel = accountViewModel,
nav = nav,
)
Button(
onClick = {
if (name.isBlank() || working) return@Button
working = true
scope.launch {
val relays = relaysCsv.split(",").map { it.trim() }.filter { it.isNotEmpty() }
val communityId = accountViewModel.account.createConcordCommunity(name.trim(), about.trim().ifBlank { null }, relays)
val communityId =
accountViewModel.account.createConcordCommunity(
name = name.trim(),
description = about.trim().ifBlank { null },
relays = relays.map { it.url },
icon = iconUrl.trim().ifBlank { null },
)
working = false
if (communityId != null) nav.newStack(Route.ConcordServer(communityId))
}
},
enabled = name.isNotBlank() && !working,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
) {
Text(stringRes(com.vitorpamplona.amethyst.R.string.concord_create_action))
}
}
}
}
@Composable
private fun SectionHeader(text: String) {
Surface(color = MaterialTheme.colorScheme.surface) {
Text(
text = text,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 8.dp),
)
}
}
+2 -1
View File
@@ -312,7 +312,8 @@
<string name="concord_create_title">New Concord Channel</string>
<string name="concord_create_name">Name</string>
<string name="concord_create_about">About (optional)</string>
<string name="concord_create_relays">Relays (comma-separated, optional)</string>
<string name="concord_create_relays">Relays</string>
<string name="concord_create_icon">Icon URL (optional)</string>
<string name="concord_create_action">Create</string>
<string name="concord_invite_action">Invite people</string>
<string name="concord_invite_title">Invite link</string>
@@ -99,7 +99,8 @@ object ConcordActions {
createdAt: Long,
description: String? = null,
relays: List<String> = emptyList(),
): NewConcordCommunity = ConcordCommunityFactory.create(ownerSigner, name, createdAt, description, relays)
icon: String? = null,
): NewConcordCommunity = ConcordCommunityFactory.create(ownerSigner, name, createdAt, description, relays, icon)
/** Opens the control-plane [wraps] into their [ControlEdition]s (drops any that don't open/parse). */
fun controlEditions(