feat(communities): prefer name() tag + add edit flow

Name display
- CommunityCard, CommunityName spinner option, FeedFilterSpinner
  renderer, and DisplayFollowingCommunityInPost now all prefer the
  NIP-72 `name` tag and fall back to the `d` tag. Previously they
  rendered the raw UUID d-tag for communities created by this app,
  which made them show up as /n/<hex> instead of the configured name.
- DisplayCommunity is now reactive via observeNote so the chip in
  NoteCompose recomposes when the community definition arrives.

Edit flow
- New Route.EditCommunity(kind, pubKeyHex, dTag) and
  EditCommunityScreen that reuses the create form via a shared
  CommunityFormScreen composable.
- NewCommunityModel gains loadFrom(CommunityDefinitionEvent) to
  preload name/description/rules/moderators/relays and to remember
  the existing d-tag + image URL. publish() reuses the existing
  d-tag so the replaceable kind-34550 event updates in place.
- Cover preview on the form supports the already-uploaded image
  (AsyncImage with remove button) and falls back to the placeholder
  if cleared.
- Owner (note author == signer) sees an "Edit" FilledTonalIconButton
  in LongCommunityActionOptions that opens the edit screen.
- Top bar becomes "Edit Community" with a Save button when editing.
This commit is contained in:
Claude
2026-04-20 21:05:06 +00:00
parent 0f84981879
commit 30646998ea
10 changed files with 255 additions and 50 deletions
@@ -94,6 +94,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.CommunitiesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.CommunitiesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.newCommunity.NewCommunityScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.newCommunity.NewCommunityScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.LongFormPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.LongFormPostScreen
@@ -225,6 +226,7 @@ fun BuildNavigation(
composableFromEnd<Route.Polls> { PollsScreen(accountViewModel, nav) } composableFromEnd<Route.Polls> { PollsScreen(accountViewModel, nav) }
composableFromEnd<Route.Communities> { CommunitiesScreen(accountViewModel, nav) } composableFromEnd<Route.Communities> { CommunitiesScreen(accountViewModel, nav) }
composableFromEnd<Route.NewCommunity> { NewCommunityScreen(accountViewModel, nav) } composableFromEnd<Route.NewCommunity> { NewCommunityScreen(accountViewModel, nav) }
composableFromEndArgs<Route.EditCommunity> { EditCommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEnd<Route.Badges> { BadgesScreen(accountViewModel, nav) } composableFromEnd<Route.Badges> { BadgesScreen(accountViewModel, nav) }
composableFromEnd<Route.ProfileBadges> { ProfileBadgesScreen(accountViewModel, nav) } composableFromEnd<Route.ProfileBadges> { ProfileBadgesScreen(accountViewModel, nav) }
composableFromBottomArgs<Route.AwardBadge> { AwardBadgeScreen(it.kind, it.pubKeyHex, it.dTag, accountViewModel, nav) } composableFromBottomArgs<Route.AwardBadge> { AwardBadgeScreen(it.kind, it.pubKeyHex, it.dTag, accountViewModel, nav) }
@@ -49,6 +49,18 @@ sealed class Route {
@Serializable object NewCommunity : Route() @Serializable object NewCommunity : Route()
@Serializable data class EditCommunity(
val kind: Int,
val pubKeyHex: HexKey,
val dTag: String,
) : Route() {
constructor(address: Address) : this(
kind = address.kind,
pubKeyHex = address.pubKeyHex,
dTag = address.dTag,
)
}
@Serializable object Badges : Route() @Serializable object Badges : Route()
@Serializable object ProfileBadges : Route() @Serializable object ProfileBadges : Route()
@@ -104,6 +104,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -351,7 +352,10 @@ fun RenderOption(
is CommunityName -> { is CommunityName -> {
val it by observeNote(option.note, accountViewModel) val it by observeNote(option.note, accountViewModel)
Text(text = "/n/${((it.note as? AddressableNote)?.dTag() ?: "")}", fontSize = Font14SP, color = MaterialTheme.colorScheme.onSurface) val addressable = it.note as? AddressableNote
val definition = addressable?.event as? CommunityDefinitionEvent
val label = definition?.name()?.ifBlank { null } ?: addressable?.dTag() ?: ""
Text(text = "/n/$label", fontSize = Font14SP, color = MaterialTheme.colorScheme.onSurface)
} }
is RelayName -> { is RelayName -> {
@@ -24,9 +24,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.ui.components.ClickableTextColor import com.vitorpamplona.amethyst.ui.components.ClickableTextColor
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -34,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
@Composable @Composable
fun DisplayFollowingCommunityInPost( fun DisplayFollowingCommunityInPost(
@@ -42,38 +46,46 @@ fun DisplayFollowingCommunityInPost(
nav: INav, nav: INav,
) { ) {
Column(HalfStartPadding) { Column(HalfStartPadding) {
Row(verticalAlignment = Alignment.CenterVertically) { DisplayCommunity(baseNote, nav) } Row(verticalAlignment = Alignment.CenterVertically) { DisplayCommunity(baseNote, accountViewModel, nav) }
} }
} }
@Composable @Composable
private fun DisplayCommunity( private fun DisplayCommunity(
note: Note, note: Note,
accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val communityTag = note.event?.communityAddress() ?: return val communityTag = note.event?.communityAddress() ?: return
val communityNote = LocalCache.getOrCreateAddressableNote(communityTag)
val communityState by observeNote(communityNote, accountViewModel)
val label = communityShortLabel(communityTag, communityState.note.event as? CommunityDefinitionEvent)
ClickableTextColor( ClickableTextColor(
getCommunityShortName(communityTag), label,
linkColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.52f), linkColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.52f),
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
) { ) {
nav.nav { nav.nav {
note.event?.communityAddress()?.let { communityTag -> note.event?.communityAddress()?.let { addr ->
Route.Community(communityTag.kind, communityTag.pubKeyHex, communityTag.dTag) Route.Community(addr.kind, addr.pubKeyHex, addr.dTag)
} }
} }
} }
} }
fun getCommunityShortName(communityAddress: Address): String { private fun communityShortLabel(
val name = address: Address,
if (communityAddress.dTag.length > 10) { definition: CommunityDefinitionEvent?,
communityAddress.dTag.take(10) + "..." ): String {
val raw = definition?.name()?.ifBlank { null } ?: address.dTag
val shortened =
if (raw.length > 12) {
raw.take(12) + "..."
} else { } else {
communityAddress.dTag.take(10) raw
} }
return "/n/$shortened"
return "/n/$name"
} }
@@ -34,6 +34,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
@@ -70,6 +71,7 @@ import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.LikeReaction
@@ -439,6 +441,9 @@ private fun LongCommunityActionOptions(
nav: INav, nav: INav,
) { ) {
Row { Row {
if (note.author?.pubkeyHex == accountViewModel.account.signer.pubKey) {
EditCommunityButton(note, nav)
}
ShareCommunityButton(accountViewModel, note, nav) ShareCommunityButton(accountViewModel, note, nav)
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing -> WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
if (isFollowing) { if (isFollowing) {
@@ -448,6 +453,22 @@ private fun LongCommunityActionOptions(
} }
} }
@Composable
fun EditCommunityButton(
note: AddressableNote,
nav: INav,
) {
FilledTonalIconButton(
onClick = { nav.nav(Route.EditCommunity(note.address)) },
) {
Icon(
imageVector = Icons.Outlined.Edit,
modifier = Size18Modifier,
contentDescription = stringRes(R.string.edit_community),
)
}
}
@Composable @Composable
fun WatchAddressableNoteFollows( fun WatchAddressableNoteFollows(
note: AddressableNote, note: AddressableNote,
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -381,7 +382,11 @@ class PeopleListName(
class CommunityName( class CommunityName(
val note: AddressableNote, val note: AddressableNote,
) : Name() { ) : Name() {
override fun name() = "/n/${(note.dTag())}" override fun name(): String {
val definition = note.event as? CommunityDefinitionEvent
val label = definition?.name()?.ifBlank { null } ?: note.dTag()
return "/n/$label"
}
} }
@Stable @Stable
@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.RelayTag import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.RelayTag
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -61,11 +62,16 @@ data class CommunityRelayEntry(
class NewCommunityModel : ViewModel() { class NewCommunityModel : ViewModel() {
var account: Account? = null var account: Account? = null
// When set, publish() reuses this d-tag so the kind-34550 replaceable event
// is updated in place (edit flow). Null in the create flow.
var existingDTag: String? = null
var isPublishing by mutableStateOf(false) var isPublishing by mutableStateOf(false)
var name by mutableStateOf("") var name by mutableStateOf("")
var description by mutableStateOf("") var description by mutableStateOf("")
var rules by mutableStateOf("") var rules by mutableStateOf("")
var existingImageUrl by mutableStateOf<String?>(null)
// Image upload state - mirrors NewBadgeModel. // Image upload state - mirrors NewBadgeModel.
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null) var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
@@ -88,6 +94,42 @@ class NewCommunityModel : ViewModel() {
this.stripMetadata = account.settings.stripLocationOnUpload this.stripMetadata = account.settings.stripLocationOnUpload
} }
/**
* Preloads the form with the current contents of [existing] so the user can edit
* the replaceable kind 34550 event. Keeps the original `d` tag so relays replace
* the previous version instead of creating a new community.
*/
fun loadFrom(existing: CommunityDefinitionEvent) {
existingDTag = existing.dTag()
name = existing.name().orEmpty()
description = existing.description().orEmpty()
rules = existing.rules().orEmpty()
existingImageUrl = existing.image()?.imageUrl
val ownerKey = account?.signer?.pubKey
moderators.clear()
existing
.moderatorKeys()
.asSequence()
.filter { it != ownerKey }
.distinct()
.forEach { pubkey ->
val user = account?.cache?.getOrCreateUser(pubkey) ?: return@forEach
if (moderators.none { it.pubkeyHex == user.pubkeyHex }) {
moderators.add(user)
}
}
relays.clear()
existing.relays().forEach { tag ->
if (relays.none { it.url == tag.url }) {
relays.add(CommunityRelayEntry(tag.url, tag.marker))
}
}
}
fun isEditing(): Boolean = existingDTag != null
fun defaultServer() = account?.settings?.defaultFileServer ?: DEFAULT_MEDIA_SERVERS[0] fun defaultServer() = account?.settings?.defaultFileServer ?: DEFAULT_MEDIA_SERVERS[0]
fun setPickedMedia(uris: ImmutableList<SelectedMedia>) { fun setPickedMedia(uris: ImmutableList<SelectedMedia>) {
@@ -156,15 +198,15 @@ class NewCommunityModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
isPublishing = true isPublishing = true
try { try {
val imageUrl = val uploadedUrl =
uploadImageIfAny(context, myAccount, onError) ?: run { if (multiOrchestrator != null) {
if (multiOrchestrator != null) { uploadImageIfAny(context, myAccount, onError) ?: return@launch
// upload failed; error was reported, bail out } else {
return@launch
}
null null
} }
val imageUrl = uploadedUrl ?: existingImageUrl
val ownerKey = myAccount.signer.pubKey val ownerKey = myAccount.signer.pubKey
val moderatorTags = val moderatorTags =
buildList { buildList {
@@ -177,6 +219,8 @@ class NewCommunityModel : ViewModel() {
val relayTags = relays.map { RelayTag(it.url, it.marker) } val relayTags = relays.map { RelayTag(it.url, it.marker) }
val dTag = existingDTag ?: Uuid.random().toString()
val definition = val definition =
myAccount.sendCommunityDefinition( myAccount.sendCommunityDefinition(
name = name.trim(), name = name.trim(),
@@ -185,7 +229,7 @@ class NewCommunityModel : ViewModel() {
image = imageUrl, image = imageUrl,
rules = rules.trim().ifBlank { null }, rules = rules.trim().ifBlank { null },
relays = relayTags.ifEmpty { null }, relays = relayTags.ifEmpty { null },
dTag = Uuid.random().toString(), dTag = dTag,
) )
if (definition == null) { if (definition == null) {
@@ -196,9 +240,11 @@ class NewCommunityModel : ViewModel() {
return@launch return@launch
} }
// Follow it so it shows up under "Mine". // Auto-follow only on the create flow; editing doesn't change the follow set.
val communityNote = myAccount.cache.getOrCreateAddressableNote(definition.address()) if (existingDTag == null) {
myAccount.follow(communityNote) val communityNote = myAccount.cache.getOrCreateAddressableNote(definition.address())
myAccount.follow(communityNote)
}
selectedServer?.let { myAccount.settings.changeDefaultFileServer(it) } selectedServer?.let { myAccount.settings.changeDefaultFileServer(it) }
myAccount.settings.changeStripLocationOnUpload(stripMetadata) myAccount.settings.changeStripLocationOnUpload(stripMetadata)
@@ -260,6 +306,8 @@ class NewCommunityModel : ViewModel() {
name = "" name = ""
description = "" description = ""
rules = "" rules = ""
existingImageUrl = null
existingDTag = null
multiOrchestrator = null multiOrchestrator = null
moderators.clear() moderators.clear()
relays.clear() relays.clear()
@@ -71,14 +71,17 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.ActionTopBar
import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar
import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
@@ -89,14 +92,30 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditF
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightPage
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.RelayTag import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.RelayTag
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NewCommunityScreen( fun NewCommunityScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) = CommunityFormScreen(editing = null, accountViewModel = accountViewModel, nav = nav)
@Composable
fun EditCommunityScreen(
editing: Address,
accountViewModel: AccountViewModel,
nav: INav,
) = CommunityFormScreen(editing = editing, accountViewModel = accountViewModel, nav = nav)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CommunityFormScreen(
editing: Address?,
accountViewModel: AccountViewModel,
nav: INav,
) { ) {
val model: NewCommunityModel = viewModel() val model: NewCommunityModel = viewModel()
val context = LocalContext.current val context = LocalContext.current
@@ -105,6 +124,15 @@ fun NewCommunityScreen(
model.init(accountViewModel.account) model.init(accountViewModel.account)
} }
LaunchedEffect(editing) {
if (editing != null) {
val note = LocalCache.getOrCreateAddressableNote(editing)
(note.event as? CommunityDefinitionEvent)?.let { model.loadFrom(it) }
} else {
model.existingDTag = null
}
}
StrippingFailureDialog(model.strippingFailureConfirmation) StrippingFailureDialog(model.strippingFailureConfirmation)
var wantsToPickImage by remember { mutableStateOf(false) } var wantsToPickImage by remember { mutableStateOf(false) }
@@ -122,21 +150,42 @@ fun NewCommunityScreen(
Scaffold( Scaffold(
topBar = { topBar = {
CreatingTopBar( val titleRes =
titleRes = R.string.new_community, if (model.isEditing()) R.string.edit_community else R.string.new_community
isActive = model::canPost, if (model.isEditing()) {
onCancel = { ActionTopBar(
model.reset() titleRes = titleRes,
nav.popBack() postRes = R.string.save,
}, isActive = model::canPost,
onPost = { onCancel = {
model.publish( model.reset()
context = context, nav.popBack()
onSuccess = { nav.popBack() }, },
onError = accountViewModel.toastManager::toast, onPost = {
) model.publish(
}, context = context,
) onSuccess = { nav.popBack() },
onError = accountViewModel.toastManager::toast,
)
},
)
} else {
CreatingTopBar(
titleRes = titleRes,
isActive = model::canPost,
onCancel = {
model.reset()
nav.popBack()
},
onPost = {
model.publish(
context = context,
onSuccess = { nav.popBack() },
onError = accountViewModel.toastManager::toast,
)
},
)
}
}, },
) { pad -> ) { pad ->
Surface( Surface(
@@ -235,18 +284,69 @@ private fun CommunityImagePicker(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onPickImage: () -> Unit, onPickImage: () -> Unit,
) { ) {
if (model.hasPickedImage()) { val existingUrl = model.existingImageUrl
model.multiOrchestrator?.let { when {
Box(modifier = Modifier.clickable(onClick = onPickImage)) { model.hasPickedImage() -> {
ShowImageUploadGallery( model.multiOrchestrator?.let {
list = it, Box(modifier = Modifier.clickable(onClick = onPickImage)) {
onDelete = { model.setPickedMedia(persistentListOf()) }, ShowImageUploadGallery(
accountViewModel = accountViewModel, list = it,
) onDelete = { model.setPickedMedia(persistentListOf()) },
accountViewModel = accountViewModel,
)
}
} }
} }
} else {
CommunityImagePlaceholder(onClick = onPickImage) !existingUrl.isNullOrBlank() -> {
ExistingCommunityCover(
url = existingUrl,
onClick = onPickImage,
onClear = { model.existingImageUrl = null },
)
}
else -> {
CommunityImagePlaceholder(onClick = onPickImage)
}
}
}
@Composable
private fun ExistingCommunityCover(
url: String,
onClick: () -> Unit,
onClear: () -> Unit,
) {
Box(
modifier =
Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
.clickable(onClick = onClick),
contentAlignment = Alignment.TopEnd,
) {
AsyncImage(
model = url,
contentDescription = null,
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
modifier =
Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outline,
shape = RoundedCornerShape(16.dp),
),
)
IconButton(onClick = onClear) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = stringRes(R.string.remove),
tint = MaterialTheme.colorScheme.onSurface,
)
}
} }
} }
@@ -96,7 +96,7 @@ fun RenderCommunitiesThumb(
RenderCommunitiesThumb( RenderCommunitiesThumb(
CommunityCard( CommunityCard(
name = noteEvent.dTag(), name = noteEvent.name()?.ifBlank { null } ?: noteEvent.dTag(),
description = noteEvent.description(), description = noteEvent.description(),
cover = noteEvent.image()?.imageUrl, cover = noteEvent.image()?.imageUrl,
moderators = noteEvent.moderatorKeys().toImmutableList(), moderators = noteEvent.moderatorKeys().toImmutableList(),
+1
View File
@@ -422,6 +422,7 @@
<string name="badges">Badges</string> <string name="badges">Badges</string>
<string name="communities">Communities</string> <string name="communities">Communities</string>
<string name="new_community">New Community</string> <string name="new_community">New Community</string>
<string name="edit_community">Edit Community</string>
<string name="new_community_name">Community name</string> <string name="new_community_name">Community name</string>
<string name="new_community_description">Description</string> <string name="new_community_description">Description</string>
<string name="new_community_rules">Rules (optional)</string> <string name="new_community_rules">Rules (optional)</string>