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:
@@ -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()
|
||||||
|
|||||||
+5
-1
@@ -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 -> {
|
||||||
|
|||||||
+23
-11
@@ -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
|
||||||
|
|||||||
+58
-10
@@ -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()
|
||||||
|
|||||||
+126
-26
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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(),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user