- Upgrades NIP-72 communities to NIP-22 comments
- Adds moderation queue feed - Adds in-app approval procedures - Adds drafts for community posts - Redesigns About us section of communities.
This commit is contained in:
@@ -186,6 +186,8 @@ import com.vitorpamplona.quartz.nip68Picture.pictureIMeta
|
|||||||
import com.vitorpamplona.quartz.nip71Video.VideoMeta
|
import com.vitorpamplona.quartz.nip71Video.VideoMeta
|
||||||
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
|
||||||
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
|
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
|
||||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
||||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||||
@@ -863,6 +865,24 @@ class Account(
|
|||||||
|
|
||||||
suspend fun unfollowGeohash(geohash: String) = sendMyPublicAndPrivateOutbox(geohashList.unfollow(geohash))
|
suspend fun unfollowGeohash(geohash: String) = sendMyPublicAndPrivateOutbox(geohashList.unfollow(geohash))
|
||||||
|
|
||||||
|
suspend fun approveCommunityPost(
|
||||||
|
post: Note,
|
||||||
|
community: AddressableNote,
|
||||||
|
) {
|
||||||
|
val commEvent = community.event as? CommunityDefinitionEvent ?: return
|
||||||
|
val postHint = post.toEventHint<Event>() ?: return
|
||||||
|
val communityHint = community.toEventHint<CommunityDefinitionEvent>() ?: return
|
||||||
|
|
||||||
|
val template = CommunityPostApprovalEvent.build(postHint, communityHint)
|
||||||
|
|
||||||
|
val signedEvent = signer.sign(template)
|
||||||
|
|
||||||
|
val relays = outboxRelays.flow.value + commEvent.relayUrls() + community.relays + (post.author?.inboxRelays() ?: emptyList())
|
||||||
|
|
||||||
|
cache.justConsumeMyOwnEvent(signedEvent)
|
||||||
|
client.send(signedEvent, relays)
|
||||||
|
}
|
||||||
|
|
||||||
fun sendAutomatic(events: List<Event>) = events.forEach { sendAutomatic(it) }
|
fun sendAutomatic(events: List<Event>) = events.forEach { sendAutomatic(it) }
|
||||||
|
|
||||||
fun sendAutomatic(event: Event?) {
|
fun sendAutomatic(event: Event?) {
|
||||||
|
|||||||
@@ -1453,6 +1453,8 @@ object LocalCache : ILocalCache {
|
|||||||
// Counts the replies
|
// Counts the replies
|
||||||
repliesTo.forEach { it.addBoost(note) }
|
repliesTo.forEach { it.addBoost(note) }
|
||||||
|
|
||||||
|
eventsApproved.forEach { it.addBoost(note) }
|
||||||
|
|
||||||
event.containedPost()?.let {
|
event.containedPost()?.let {
|
||||||
checkDeletionAndConsume(it, relay, false)
|
checkDeletionAndConsume(it, relay, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
|||||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
|
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||||
@@ -64,6 +65,7 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
|||||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.utils.anyAsync
|
import com.vitorpamplona.quartz.utils.anyAsync
|
||||||
import com.vitorpamplona.quartz.utils.containsAny
|
import com.vitorpamplona.quartz.utils.containsAny
|
||||||
@@ -201,6 +203,16 @@ open class Note(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun relayHintUrl(): NormalizedRelayUrl? {
|
fun relayHintUrl(): NormalizedRelayUrl? {
|
||||||
|
val noteEvent = event
|
||||||
|
val communityPostRelays =
|
||||||
|
when (noteEvent) {
|
||||||
|
is CommunityDefinitionEvent -> noteEvent.relayUrls().ifEmpty { null }?.toSet()
|
||||||
|
is IsInPublicChatChannel -> LocalCache.getAnyChannel(this)?.relays()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!communityPostRelays.isNullOrEmpty()) return communityPostRelays.firstOrNull()
|
||||||
|
|
||||||
val currentOutbox = author?.outboxRelays()?.toSet()
|
val currentOutbox = author?.outboxRelays()?.toSet()
|
||||||
|
|
||||||
return if (relays.isNotEmpty()) {
|
return if (relays.isNotEmpty()) {
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ class ThreadAssembler {
|
|||||||
): Note? {
|
): Note? {
|
||||||
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
|
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
|
||||||
|
|
||||||
if (note.event is RepostEvent || note.event is GenericRepostEvent) return note
|
val noteEvent = note.event
|
||||||
|
|
||||||
|
if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) return note
|
||||||
|
|
||||||
testedNotes.add(note)
|
testedNotes.add(note)
|
||||||
|
|
||||||
val markedAsRoot =
|
val markedAsRoot =
|
||||||
note.event
|
noteEvent
|
||||||
?.tags
|
?.tags
|
||||||
?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }
|
?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }
|
||||||
?.getOrNull(1)
|
?.getOrNull(1)
|
||||||
@@ -52,7 +54,10 @@ class ThreadAssembler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasNoReplyTo = note.replyTo?.reversed()?.firstOrNull { it.replyTo?.isEmpty() == true }
|
val hasNoReplyTo =
|
||||||
|
note.replyTo?.lastOrNull {
|
||||||
|
it.replyTo?.isEmpty() == true
|
||||||
|
}
|
||||||
if (hasNoReplyTo != null) return hasNoReplyTo
|
if (hasNoReplyTo != null) return hasNoReplyTo
|
||||||
|
|
||||||
// recursive
|
// recursive
|
||||||
|
|||||||
+51
@@ -29,6 +29,11 @@ import com.vitorpamplona.amethyst.model.NoteState
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.isForCommunity
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
@@ -356,3 +361,49 @@ fun observeNoteEdits(
|
|||||||
.stateFlow
|
.stateFlow
|
||||||
.collectAsStateWithLifecycle()
|
.collectAsStateWithLifecycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun observeCommunityApprovalNeedStatus(
|
||||||
|
note: Note,
|
||||||
|
community: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
): State<Boolean?> {
|
||||||
|
// Subscribe in the relay for changes in this note.
|
||||||
|
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||||
|
|
||||||
|
// Subscribe in the LocalCache for changes that arrive in the device
|
||||||
|
val flow =
|
||||||
|
remember(note, community) {
|
||||||
|
combine(
|
||||||
|
community.flow().metadata.stateFlow,
|
||||||
|
note.flow().boosts.stateFlow,
|
||||||
|
) { communityMetadata, boosts ->
|
||||||
|
(communityMetadata.note.event as? CommunityDefinitionEvent)?.let { communityDefEvent ->
|
||||||
|
val moderators = communityDefEvent.moderatorKeys().toSet()
|
||||||
|
|
||||||
|
if (note.author?.pubkeyHex in moderators) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val isModerator = accountViewModel.account.userProfile().pubkeyHex in moderators
|
||||||
|
|
||||||
|
if (isModerator) {
|
||||||
|
val wasAlreadyApproved =
|
||||||
|
note.boosts.any {
|
||||||
|
val approvalEvent = it.event
|
||||||
|
(approvalEvent is CommunityPostApprovalEvent || approvalEvent is RepostEvent || approvalEvent is GenericRepostEvent) &&
|
||||||
|
approvalEvent.pubKey in moderators &&
|
||||||
|
approvalEvent.isForCommunity(community.idHex)
|
||||||
|
}
|
||||||
|
!wasAlreadyApproved
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe in the LocalCache for changes that arrive in the device
|
||||||
|
return flow.collectAsStateWithLifecycle(false)
|
||||||
|
}
|
||||||
|
|||||||
+2
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
|||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||||
@@ -45,6 +46,7 @@ val RepliesAndReactionsToAddressesKinds1 =
|
|||||||
ReportEvent.KIND,
|
ReportEvent.KIND,
|
||||||
LnZapEvent.KIND,
|
LnZapEvent.KIND,
|
||||||
PollNoteEvent.KIND,
|
PollNoteEvent.KIND,
|
||||||
|
CommentEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
val PostsAndChatMessagesToAddresses =
|
val PostsAndChatMessagesToAddresses =
|
||||||
|
|||||||
+4
-2
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
|||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||||
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||||
@@ -51,7 +52,7 @@ val RepliesAndReactionsKinds =
|
|||||||
PollNoteEvent.KIND,
|
PollNoteEvent.KIND,
|
||||||
OtsEvent.KIND,
|
OtsEvent.KIND,
|
||||||
TextNoteModificationEvent.KIND,
|
TextNoteModificationEvent.KIND,
|
||||||
GitReplyEvent.KIND,
|
CommentEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
val RepliesAndReactionsKinds2 =
|
val RepliesAndReactionsKinds2 =
|
||||||
@@ -60,6 +61,7 @@ val RepliesAndReactionsKinds2 =
|
|||||||
NIP90ContentDiscoveryResponseEvent.KIND,
|
NIP90ContentDiscoveryResponseEvent.KIND,
|
||||||
NIP90StatusEvent.KIND,
|
NIP90StatusEvent.KIND,
|
||||||
TorrentCommentEvent.KIND,
|
TorrentCommentEvent.KIND,
|
||||||
|
GitReplyEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun filterRepliesAndReactionsToNotes(
|
fun filterRepliesAndReactionsToNotes(
|
||||||
@@ -108,7 +110,7 @@ fun filterRepliesAndReactionsToNotes(
|
|||||||
relay = relay,
|
relay = relay,
|
||||||
filter =
|
filter =
|
||||||
Filter(
|
Filter(
|
||||||
kinds = listOf(TextNoteEvent.KIND),
|
kinds = listOf(TextNoteEvent.KIND, CommentEvent.KIND),
|
||||||
tags = mapOf("q" to sortedList),
|
tags = mapOf("q" to sortedList),
|
||||||
since = since,
|
since = since,
|
||||||
limit = 1000,
|
limit = 1000,
|
||||||
|
|||||||
+2
-1
@@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.PaddingHorizontal12Modifier
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@ fun SensitivityWarning(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ContentWarningNote(onDismiss: () -> Unit) {
|
fun ContentWarningNote(onDismiss: () -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
Row(modifier = Modifier.padding(horizontal = 12.dp)) {
|
Row(modifier = PaddingHorizontal12Modifier) {
|
||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
@@ -49,8 +48,6 @@ fun FeedLoaded(
|
|||||||
) {
|
) {
|
||||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = FeedPadding,
|
contentPadding = FeedPadding,
|
||||||
state = listState,
|
state = listState,
|
||||||
|
|||||||
+9
-1
@@ -64,9 +64,10 @@ fun DisappearingScaffold(
|
|||||||
bottomBar: (@Composable () -> Unit)? = null,
|
bottomBar: (@Composable () -> Unit)? = null,
|
||||||
floatingButton: (@Composable () -> Unit)? = null,
|
floatingButton: (@Composable () -> Unit)? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
|
isActive: () -> Boolean = { true },
|
||||||
mainContent: @Composable (padding: PaddingValues) -> Unit,
|
mainContent: @Composable (padding: PaddingValues) -> Unit,
|
||||||
) {
|
) {
|
||||||
val shouldShow = remember { mutableStateOf(true) }
|
val shouldShow = remember { mutableStateOf(isActive()) }
|
||||||
|
|
||||||
val modifier =
|
val modifier =
|
||||||
if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) {
|
if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) {
|
||||||
@@ -80,6 +81,13 @@ fun DisappearingScaffold(
|
|||||||
available: Offset,
|
available: Offset,
|
||||||
source: NestedScrollSource,
|
source: NestedScrollSource,
|
||||||
): Offset {
|
): Offset {
|
||||||
|
if (!isActive()) {
|
||||||
|
if (!shouldShow.value) {
|
||||||
|
shouldShow.value = true
|
||||||
|
}
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
val newOffset = bottomBarOffsetHeightPx.floatValue + available.y
|
val newOffset = bottomBarOffsetHeightPx.floatValue + available.y
|
||||||
|
|
||||||
if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) {
|
if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) {
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -56,6 +58,7 @@ import com.vitorpamplona.amethyst.model.FeatureSetType
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||||
@@ -139,7 +142,9 @@ import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing10dp
|
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
@@ -164,7 +169,6 @@ import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
|
|||||||
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableKind
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
|
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
|
||||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||||
@@ -212,7 +216,9 @@ import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
|
|||||||
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
|
||||||
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
|
||||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||||
@@ -552,6 +558,8 @@ fun InnerNoteWithReactions(
|
|||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RenderApprovalIfNeeded(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,6 +585,47 @@ fun InnerNoteWithReactions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderApprovalIfNeeded(
|
||||||
|
baseNote: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
if (baseNote.isNewThread()) {
|
||||||
|
val communityAddress =
|
||||||
|
remember(baseNote) {
|
||||||
|
baseNote.event?.communityAddress()
|
||||||
|
}
|
||||||
|
communityAddress?.let {
|
||||||
|
LoadAddressableNote(it, accountViewModel) { community ->
|
||||||
|
if (community != null) {
|
||||||
|
val showApproveButton by observeCommunityApprovalNeedStatus(baseNote, community, accountViewModel)
|
||||||
|
if (showApproveButton == true) {
|
||||||
|
RenderApproveButton(baseNote, community, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderApproveButton(
|
||||||
|
post: Note,
|
||||||
|
community: AddressableNote,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth().defaultMinSize(minWidth = Size10dp, minHeight = Size30dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
onClick = {
|
||||||
|
accountViewModel.approveCommunityPost(post, community)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text("Approve")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoteBody(
|
fun NoteBody(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
@@ -1090,14 +1139,7 @@ fun FirstUserInfoRow(
|
|||||||
) {
|
) {
|
||||||
Row(verticalAlignment = CenterVertically, modifier = UserNameRowHeight) {
|
Row(verticalAlignment = CenterVertically, modifier = UserNameRowHeight) {
|
||||||
val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
||||||
|
val isDraft = baseNote.isDraft()
|
||||||
val isCommunityPost by
|
|
||||||
remember(baseNote) {
|
|
||||||
derivedStateOf {
|
|
||||||
baseNote.event?.isTaggedAddressableKind(CommunityDefinitionEvent.KIND) == true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val textColor = if (isRepost) MaterialTheme.colorScheme.grayText else Color.Unspecified
|
val textColor = if (isRepost) MaterialTheme.colorScheme.grayText else Color.Unspecified
|
||||||
|
|
||||||
if (showAuthorPicture) {
|
if (showAuthorPicture) {
|
||||||
@@ -1108,11 +1150,29 @@ fun FirstUserInfoRow(
|
|||||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel)
|
NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDraft) {
|
||||||
|
ObserveDraftEvent(baseNote, accountViewModel) { draftNote ->
|
||||||
|
val isCommunityPost by remember(draftNote) {
|
||||||
|
derivedStateOf { draftNote.event?.isACommunityPost() == true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCommunityPost) {
|
||||||
|
DisplayFollowingCommunityInPost(draftNote, accountViewModel, nav)
|
||||||
|
} else {
|
||||||
|
DisplayFollowingHashtagsInPost(draftNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val isCommunityPost by remember(baseNote) {
|
||||||
|
derivedStateOf { baseNote.event?.isACommunityPost() == true }
|
||||||
|
}
|
||||||
|
|
||||||
if (isCommunityPost) {
|
if (isCommunityPost) {
|
||||||
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
|
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
|
||||||
} else {
|
} else {
|
||||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isRepost) {
|
if (isRepost) {
|
||||||
BoostedMark()
|
BoostedMark()
|
||||||
@@ -1120,7 +1180,7 @@ fun FirstUserInfoRow(
|
|||||||
|
|
||||||
CheckAndDisplayEditStatus(editState)
|
CheckAndDisplayEditStatus(editState)
|
||||||
|
|
||||||
if (baseNote.isDraft()) {
|
if (isDraft) {
|
||||||
DisplayDraft()
|
DisplayDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1192,7 +1252,7 @@ fun observeEdits(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BadgeBox(
|
fun BadgeBox(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
@@ -1207,7 +1267,7 @@ private fun BadgeBox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderAuthorImages(
|
fun RenderAuthorImages(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
|
|||||||
+12
-9
@@ -34,9 +34,8 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.tags.addressables.ATag
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.getTagOfAddressableKind
|
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayFollowingCommunityInPost(
|
fun DisplayFollowingCommunityInPost(
|
||||||
@@ -55,11 +54,15 @@ private fun DisplayCommunity(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val communityTag =
|
val communityTag =
|
||||||
remember(note) { note.event?.getTagOfAddressableKind(CommunityDefinitionEvent.KIND) } ?: return
|
remember(note) { note.event?.communityAddress() } ?: return
|
||||||
|
|
||||||
val displayTag =
|
val displayTag =
|
||||||
remember(note) {
|
remember(note) {
|
||||||
buildLinkString(getCommunityShortName(communityTag)) { nav.nav(Route.Community(communityTag.kind, communityTag.pubKeyHex, communityTag.dTag)) }
|
buildLinkString(
|
||||||
|
getCommunityShortName(communityTag),
|
||||||
|
) {
|
||||||
|
nav.nav(Route.Community(communityTag.kind, communityTag.pubKeyHex, communityTag.dTag))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -75,12 +78,12 @@ private fun DisplayCommunity(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCommunityShortName(communityTag: ATag): String {
|
fun getCommunityShortName(communityAddress: Address): String {
|
||||||
val name =
|
val name =
|
||||||
if (communityTag.dTag.length > 10) {
|
if (communityAddress.dTag.length > 10) {
|
||||||
communityTag.dTag.take(10) + "..."
|
communityAddress.dTag.take(10) + "..."
|
||||||
} else {
|
} else {
|
||||||
communityTag.dTag.take(10)
|
communityAddress.dTag.take(10)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "/n/$name"
|
return "/n/$name"
|
||||||
|
|||||||
+25
-3
@@ -86,6 +86,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
|||||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.scope
|
import com.vitorpamplona.quartz.nip73ExternalIds.scope
|
||||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
||||||
@@ -252,8 +253,7 @@ open class CommentPostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDraft(draftEvent: CommentEvent) {
|
private fun loadFromDraft(draftEvent: CommentEvent) {
|
||||||
val scope = draftEvent.scope() ?: return
|
this.externalIdentity = draftEvent.scope()
|
||||||
this.externalIdentity = scope
|
|
||||||
|
|
||||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||||
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
|
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
|
||||||
@@ -278,9 +278,15 @@ open class CommentPostViewModel :
|
|||||||
zapRaiserAmount.value = zapraiser
|
zapRaiserAmount.value = zapraiser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val replyAddress = draftEvent.replyAddress()
|
||||||
|
|
||||||
|
if (replyAddress.isNotEmpty()) {
|
||||||
|
replyingTo = LocalCache.getOrCreateAddressableNote(replyAddress.first())
|
||||||
|
} else {
|
||||||
draftEvent.replyingTo()?.let {
|
draftEvent.replyingTo()?.let {
|
||||||
replyingTo = LocalCache.getOrCreateNote(it)
|
replyingTo = LocalCache.getOrCreateNote(it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wantsToAddGeoHash = draftEvent.hasGeohashes()
|
wantsToAddGeoHash = draftEvent.hasGeohashes()
|
||||||
|
|
||||||
@@ -356,6 +362,7 @@ open class CommentPostViewModel :
|
|||||||
val contentWarningReason = if (wantsToMarkAsSensitive) "" else null
|
val contentWarningReason = if (wantsToMarkAsSensitive) "" else null
|
||||||
|
|
||||||
val replyingTo = replyingTo
|
val replyingTo = replyingTo
|
||||||
|
val replyingToEvent = replyingTo?.event
|
||||||
|
|
||||||
val template =
|
val template =
|
||||||
if (replyingTo != null) {
|
if (replyingTo != null) {
|
||||||
@@ -365,7 +372,22 @@ open class CommentPostViewModel :
|
|||||||
msg = tagger.message,
|
msg = tagger.message,
|
||||||
replyingTo = eventHint,
|
replyingTo = eventHint,
|
||||||
) {
|
) {
|
||||||
tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) }
|
val notifyPTags = tagger.pTags?.let { pTagList -> pTagList.map { it.toPTag() } } ?: emptyList()
|
||||||
|
|
||||||
|
val extraNotificationAuthors =
|
||||||
|
if (replyingToEvent is CommunityDefinitionEvent) {
|
||||||
|
replyingToEvent.moderatorKeys().mapNotNull {
|
||||||
|
if (it != replyingToEvent.pubKey) {
|
||||||
|
accountViewModel.checkGetOrCreateUser(it)?.toPTag()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
notify((notifyPTags + extraNotificationAuthors).distinctBy { it.pubKey })
|
||||||
|
|
||||||
hashtags(findHashtags(tagger.message))
|
hashtags(findHashtags(tagger.message))
|
||||||
references(findURLs(tagger.message))
|
references(findURLs(tagger.message))
|
||||||
|
|||||||
+223
-93
@@ -20,16 +20,27 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.note.types
|
package com.vitorpamplona.amethyst.ui.note.types
|
||||||
|
|
||||||
|
import android.R.attr.onClick
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
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.FilledTonalIconButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -37,14 +48,18 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
@@ -52,8 +67,11 @@ import com.vitorpamplona.amethyst.model.FeatureSetType
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||||
|
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.EmptyNav.nav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
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
|
||||||
@@ -62,9 +80,10 @@ import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
|||||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -74,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
@@ -81,12 +101,12 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import java.util.Locale
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderCommunity(
|
fun RenderCommunity(
|
||||||
@@ -110,6 +130,16 @@ fun RenderCommunity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Title(title: String) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LongCommunityHeader(
|
fun LongCommunityHeader(
|
||||||
baseNote: AddressableNote,
|
baseNote: AddressableNote,
|
||||||
@@ -118,82 +148,123 @@ fun LongCommunityHeader(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val noteEvent by observeNoteEvent<CommunityDefinitionEvent>(baseNote, accountViewModel)
|
val noteEvent by observeNoteEvent<CommunityDefinitionEvent>(baseNote, accountViewModel)
|
||||||
|
val callbackUri = baseNote.toNostrUri()
|
||||||
|
|
||||||
Row(
|
|
||||||
lineModifier,
|
|
||||||
) {
|
|
||||||
val rulesLabel = stringRes(id = R.string.rules)
|
|
||||||
val summary =
|
|
||||||
remember(noteEvent) {
|
|
||||||
val subject = noteEvent?.subject()?.ifEmpty { null }
|
|
||||||
val body = noteEvent?.description()?.ifBlank { null }
|
|
||||||
val rules = noteEvent?.rules()?.ifBlank { null }
|
|
||||||
|
|
||||||
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
|
|
||||||
if (rules == null) {
|
|
||||||
"### $subject\n$body"
|
|
||||||
} else {
|
|
||||||
"### $subject\n$body\n\n### $rulesLabel\n\n$rules"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rules == null) {
|
|
||||||
body
|
|
||||||
} else {
|
|
||||||
"$body\n\n$rulesLabel\n$rules"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
Modifier.weight(1f),
|
|
||||||
) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
val defaultBackground = MaterialTheme.colorScheme.background
|
val defaultBackground = MaterialTheme.colorScheme.background
|
||||||
val background = remember { mutableStateOf(defaultBackground) }
|
val background = remember { mutableStateOf(defaultBackground) }
|
||||||
|
|
||||||
|
val description = noteEvent?.description()?.ifBlank { null }
|
||||||
|
val guidelines = noteEvent?.rules()?.ifBlank { null }
|
||||||
|
val image = noteEvent?.image()?.imageUrl
|
||||||
|
val tagList = noteEvent?.tags?.toImmutableListOfLists() ?: EmptyTagList
|
||||||
|
|
||||||
|
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||||
|
Box(contentAlignment = Alignment.TopEnd) {
|
||||||
|
image?.let {
|
||||||
|
MyAsyncImage(
|
||||||
|
imageUrl = it,
|
||||||
|
contentDescription =
|
||||||
|
stringRes(
|
||||||
|
R.string.preview_card_image_for,
|
||||||
|
it,
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
mainImageModifier = Modifier,
|
||||||
|
loadedImageModifier = Modifier.fillMaxWidth(),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) },
|
||||||
|
onError = { DefaultImageHeader(baseNote, accountViewModel) },
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
DefaultImageHeader(baseNote, accountViewModel)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
) {
|
||||||
|
LongCommunityActionOptions(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(lineModifier) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = noteEvent?.name() ?: baseNote.dTag(),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Title(title = stringRes(R.string.about_us))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Column(Modifier.padding(16.dp, 0.dp, 16.dp, 0.dp)) {
|
||||||
TranslatableRichTextViewer(
|
TranslatableRichTextViewer(
|
||||||
content = summary ?: stringRes(id = R.string.community_no_descriptor),
|
content = description ?: stringRes(id = R.string.community_no_descriptor),
|
||||||
|
id = baseNote.idHex + "description",
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
RichTextViewer(
|
||||||
|
it,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
canPreview = false,
|
canPreview = false,
|
||||||
quotesLeft = 1,
|
quotesLeft = 1,
|
||||||
tags = EmptyTagList,
|
tags = tagList,
|
||||||
backgroundColor = background,
|
backgroundColor = background,
|
||||||
id = baseNote.idHex,
|
callbackUri = callbackUri,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guidelines?.let {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Title(title = stringRes(R.string.guidelines))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Column(Modifier.padding(16.dp, 0.dp, 16.dp, 0.dp)) {
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
content = guidelines,
|
||||||
|
id = baseNote.idHex + "guidelines",
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
RichTextViewer(
|
||||||
|
it,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
canPreview = false,
|
||||||
|
quotesLeft = 1,
|
||||||
|
tags = tagList,
|
||||||
|
backgroundColor = background,
|
||||||
|
callbackUri = callbackUri,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
noteEvent?.let {
|
noteEvent?.let {
|
||||||
if (it.hasHashtags()) {
|
if (it.hasHashtags()) {
|
||||||
DisplayUncitedHashtags(
|
DisplayUncitedHashtags(
|
||||||
event = it,
|
event = it,
|
||||||
content = summary ?: "",
|
content = description ?: "",
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Row {
|
Title(title = stringRes(R.string.owner))
|
||||||
Spacer(DoubleHorzSpacer)
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
LongCommunityActionOptions(baseNote, accountViewModel, nav)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
lineModifier,
|
Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
|
||||||
text = stringRes(id = R.string.owner),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.width(75.dp),
|
|
||||||
)
|
|
||||||
Spacer(DoubleHorzSpacer)
|
|
||||||
NoteAuthorPicture(baseNote, Size25dp, accountViewModel = accountViewModel, nav = nav)
|
NoteAuthorPicture(baseNote, Size25dp, accountViewModel = accountViewModel, nav = nav)
|
||||||
Spacer(DoubleHorzSpacer)
|
Spacer(DoubleHorzSpacer)
|
||||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f), accountViewModel = accountViewModel)
|
NoteUsernameDisplay(baseNote, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||||
@@ -211,49 +282,35 @@ fun LongCommunityHeader(
|
|||||||
|
|
||||||
if (participants != null) {
|
if (participants != null) {
|
||||||
accountViewModel.loadParticipants(participants) { newParticipantUsers ->
|
accountViewModel.loadParticipants(participants) { newParticipantUsers ->
|
||||||
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
val noOwner = newParticipantUsers.filter { it.second != baseNote.author }.toImmutableList()
|
||||||
participantUsers = newParticipantUsers
|
if (!equalImmutableLists(noOwner, participantUsers)) {
|
||||||
|
participantUsers = noOwner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (participantUsers.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Title(title = stringRes(R.string.moderators))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
participantUsers.forEach {
|
participantUsers.forEach {
|
||||||
|
Column(Modifier.padding(vertical = 5.dp).clickable { nav.nav(routeFor(it.second)) }) {
|
||||||
Row(
|
Row(
|
||||||
lineModifier.clickable { nav.nav(routeFor(it.second)) },
|
Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
it.first.role?.let { it1 ->
|
|
||||||
Text(
|
|
||||||
text =
|
|
||||||
it1.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
|
||||||
},
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.width(75.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(DoubleHorzSpacer)
|
|
||||||
ClickableUserPicture(it.second, Size25dp, accountViewModel)
|
ClickableUserPicture(it.second, Size25dp, accountViewModel)
|
||||||
Spacer(DoubleHorzSpacer)
|
Spacer(DoubleHorzSpacer)
|
||||||
UsernameDisplay(it.second, Modifier.weight(1f), accountViewModel = accountViewModel)
|
UsernameDisplay(it.second, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Spacer(modifier = Modifier.height(15.dp))
|
||||||
lineModifier,
|
}
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringRes(id = R.string.created_at),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.width(75.dp),
|
|
||||||
)
|
|
||||||
Spacer(DoubleHorzSpacer)
|
|
||||||
NormalTimeAgo(baseNote = baseNote, Modifier.weight(1f))
|
|
||||||
MoreOptionsButton(baseNote, null, accountViewModel, nav)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +365,47 @@ fun ShortCommunityHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShortCommunityActionOptions(
|
fun ShortCommunityHeaderNoActions(
|
||||||
|
baseNote: AddressableNote,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val noteEvent by observeNoteEvent<CommunityDefinitionEvent>(baseNote, accountViewModel)
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
noteEvent?.image()?.let {
|
||||||
|
RobohashFallbackAsyncImage(
|
||||||
|
robot = baseNote.idHex,
|
||||||
|
model = it.imageUrl,
|
||||||
|
contentDescription = stringRes(R.string.profile_image),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = HeaderPictureModifier,
|
||||||
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
|
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.height(Size35dp)
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = noteEvent?.name() ?: baseNote.dTag(),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShortCommunityActionOptions(
|
||||||
note: AddressableNote,
|
note: AddressableNote,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
@@ -346,12 +443,15 @@ private fun LongCommunityActionOptions(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
|
Row {
|
||||||
|
ShareCommunityButton(accountViewModel, note, nav)
|
||||||
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
|
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
|
||||||
if (isFollowing) {
|
if (isFollowing) {
|
||||||
LeaveCommunityButton(accountViewModel, note, nav)
|
LeaveCommunityButton(accountViewModel, note, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WatchAddressableNoteFollows(
|
fun WatchAddressableNoteFollows(
|
||||||
@@ -388,18 +488,48 @@ fun LeaveCommunityButton(
|
|||||||
note: AddressableNote,
|
note: AddressableNote,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
FilledTonalButton(
|
||||||
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
onClick = { accountViewModel.unfollow(note) },
|
onClick = { accountViewModel.unfollow(note) },
|
||||||
shape = ButtonBorder,
|
|
||||||
colors =
|
|
||||||
ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
contentPadding = ButtonPadding,
|
contentPadding = ButtonPadding,
|
||||||
) {
|
) {
|
||||||
Text(text = stringRes(R.string.leave), color = Color.White)
|
Text(text = stringRes(R.string.leave))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShareCommunityButton(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
note: AddressableNote,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val actContext = LocalContext.current
|
||||||
|
|
||||||
|
FilledTonalIconButton(
|
||||||
|
onClick = {
|
||||||
|
val sendIntent =
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
externalLinkForNote(note),
|
||||||
|
)
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TITLE,
|
||||||
|
stringRes(actContext, R.string.quick_action_share_browser_link),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareIntent =
|
||||||
|
Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share))
|
||||||
|
ContextCompat.startActivity(actContext, shareIntent, null)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Share,
|
||||||
|
modifier = Size18Modifier,
|
||||||
|
contentDescription = stringRes(R.string.quick_action_share),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
|||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.FeedLoaded
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||||
@@ -102,8 +103,7 @@ fun RenderFeedState(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
routeForLastRead: String?,
|
routeForLastRead: String?,
|
||||||
onLoaded: @Composable (FeedState.Loaded) -> Unit = {
|
onLoaded: @Composable (FeedState.Loaded) -> Unit = {
|
||||||
com.vitorpamplona.amethyst.ui.feeds
|
FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav)
|
||||||
.FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav)
|
|
||||||
},
|
},
|
||||||
onEmpty: @Composable () -> Unit = { FeedEmpty { viewModel.invalidateData() } },
|
onEmpty: @Composable () -> Unit = { FeedEmpty { viewModel.invalidateData() } },
|
||||||
onError: @Composable (String) -> Unit = { FeedError(it) { viewModel.invalidateData() } },
|
onError: @Composable (String) -> Unit = { FeedError(it) { viewModel.invalidateData() } },
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
|||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
@@ -123,11 +124,8 @@ class FollowListState(
|
|||||||
|
|
||||||
val livePeopleListsFlow = MutableStateFlow(emptyList<FeedDefinition>())
|
val livePeopleListsFlow = MutableStateFlow(emptyList<FeedDefinition>())
|
||||||
|
|
||||||
fun updateFeedWith(newNotes: Set<Note>) {
|
fun hasItemInNoteList(notes: Set<Note>): Boolean =
|
||||||
checkNotInMainThread()
|
notes.any { it ->
|
||||||
|
|
||||||
val hasNewList =
|
|
||||||
newNotes.any { it ->
|
|
||||||
val noteEvent = it.event
|
val noteEvent = it.event
|
||||||
|
|
||||||
noteEvent?.pubKey == account.userProfile().pubkeyHex &&
|
noteEvent?.pubKey == account.userProfile().pubkeyHex &&
|
||||||
@@ -141,18 +139,25 @@ class FollowListState(
|
|||||||
(
|
(
|
||||||
noteEvent is DeletionEvent &&
|
noteEvent is DeletionEvent &&
|
||||||
(
|
(
|
||||||
noteEvent.deleteEventIds().any {
|
noteEvent.deleteEventIds().any { LocalCache.getNoteIfExists(it)?.event is PeopleListEvent } ||
|
||||||
LocalCache.getNoteIfExists(it)?.event is PeopleListEvent
|
noteEvent.deleteAddresses().any { it.kind == PeopleListEvent.KIND }
|
||||||
} ||
|
|
||||||
noteEvent.deleteAddresses().any {
|
|
||||||
it.kind == PeopleListEvent.KIND
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasNewList) {
|
fun deleteFromFeed(deletedNotes: Set<Note>) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
if (hasItemInNoteList(deletedNotes)) {
|
||||||
|
livePeopleListsFlow.tryEmit(getPeopleLists())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFeedWith(newNotes: Set<Note>) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
if (hasItemInNoteList(newNotes)) {
|
||||||
livePeopleListsFlow.tryEmit(getPeopleLists())
|
livePeopleListsFlow.tryEmit(getPeopleLists())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +168,7 @@ class FollowListState(
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val communities =
|
val communities =
|
||||||
communityList.mapNotNull {
|
communityList.map {
|
||||||
LocalCache.getOrCreateAddressableNote(it.address).let { communityNote ->
|
LocalCache.getOrCreateAddressableNote(it.address).let { communityNote ->
|
||||||
TagFeedDefinition(
|
TagFeedDefinition(
|
||||||
"Community/${communityNote.idHex}",
|
"Community/${communityNote.idHex}",
|
||||||
@@ -377,6 +382,7 @@ val DEFAULT_FEED_KINDS =
|
|||||||
val DEFAULT_COMMUNITY_FEEDS =
|
val DEFAULT_COMMUNITY_FEEDS =
|
||||||
listOf(
|
listOf(
|
||||||
TextNoteEvent.KIND,
|
TextNoteEvent.KIND,
|
||||||
|
CommentEvent.KIND,
|
||||||
LongTextNoteEvent.KIND,
|
LongTextNoteEvent.KIND,
|
||||||
ClassifiedsEvent.KIND,
|
ClassifiedsEvent.KIND,
|
||||||
HighlightEvent.KIND,
|
HighlightEvent.KIND,
|
||||||
|
|||||||
+5
@@ -774,6 +774,11 @@ class AccountViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun approveCommunityPost(
|
||||||
|
post: Note,
|
||||||
|
community: AddressableNote,
|
||||||
|
) = runIOCatching { account.approveCommunityPost(post, community) }
|
||||||
|
|
||||||
fun follow(community: AddressableNote) = runIOCatching { account.follow(community) }
|
fun follow(community: AddressableNote) = runIOCatching { account.follow(community) }
|
||||||
|
|
||||||
fun follow(channel: PublicChatChannel) = runIOCatching { account.follow(channel) }
|
fun follow(channel: PublicChatChannel) = runIOCatching { account.follow(channel) }
|
||||||
|
|||||||
+116
-34
@@ -20,29 +20,56 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities
|
||||||
|
|
||||||
|
import android.R.attr.onClick
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults.contentPadding
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.TabRow
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TitleIconModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.LongCommunityHeader
|
import com.vitorpamplona.amethyst.ui.note.types.LongCommunityHeader
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.ShortCommunityHeader
|
import com.vitorpamplona.amethyst.ui.note.types.ShortCommunityActionOptions
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.ShortCommunityHeaderNoActions
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal.CommunityFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal.CommunityFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal.CommunityModerationFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssemblerSubscription
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssemblerSubscription
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.isLight
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommunityScreen(
|
fun CommunityScreen(
|
||||||
@@ -77,61 +104,116 @@ fun PrepareViewModelsCommunityScreen(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
CommunityScreen(note, followsFeedViewModel, accountViewModel, nav)
|
val modFeedViewModel: CommunityModerationFeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = note.idHex + "CommunityModerationFeedViewModel",
|
||||||
|
factory =
|
||||||
|
CommunityModerationFeedViewModel.Factory(
|
||||||
|
note,
|
||||||
|
accountViewModel.account,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
CommunityScreen(note, followsFeedViewModel, modFeedViewModel, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CommunityScreen(
|
fun CommunityScreen(
|
||||||
note: AddressableNote,
|
note: AddressableNote,
|
||||||
feedViewModel: CommunityFeedViewModel,
|
feedViewModel: CommunityFeedViewModel,
|
||||||
|
modFeedViewModel: CommunityModerationFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
WatchLifecycleAndUpdateModel(feedViewModel)
|
WatchLifecycleAndUpdateModel(feedViewModel)
|
||||||
|
WatchLifecycleAndUpdateModel(modFeedViewModel)
|
||||||
CommunityFilterAssemblerSubscription(note, accountViewModel.dataSources().community)
|
CommunityFilterAssemblerSubscription(note, accountViewModel.dataSources().community)
|
||||||
|
|
||||||
|
val pagerState = rememberForeverPagerState(note.idHex + "CommunityScreenPagerState") { 2 }
|
||||||
|
val expanded = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
DisappearingScaffold(
|
DisappearingScaffold(
|
||||||
isInvertedLayout = false,
|
isInvertedLayout = false,
|
||||||
|
isActive = { !expanded.value },
|
||||||
topBar = {
|
topBar = {
|
||||||
CommunityTopBar(note.address, accountViewModel, nav)
|
Column(
|
||||||
|
Modifier.clickable { expanded.value = !expanded.value },
|
||||||
|
) {
|
||||||
|
ShorterTopAppBar(
|
||||||
|
title = {
|
||||||
|
ShortCommunityHeaderNoActions(note, accountViewModel, nav)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = nav::popBack) { ArrowBackIcon() }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.height(Size35dp)
|
||||||
|
.padding(start = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
ShortCommunityActionOptions(note, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (expanded.value) {
|
||||||
|
val elevation = if (MaterialTheme.colorScheme.isLight) 1.dp else 10.dp
|
||||||
|
Surface(modifier = Modifier.fillMaxWidth(), tonalElevation = elevation) {
|
||||||
|
LongCommunityHeader(baseNote = note, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TabRow(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
modifier = TabRowHeight,
|
||||||
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 0,
|
||||||
|
text = { Text(text = stringRes(R.string.feed)) },
|
||||||
|
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
|
||||||
|
)
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 1,
|
||||||
|
text = { Text(text = stringRes(R.string.mod_queue)) },
|
||||||
|
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
floatingButton = {
|
floatingButton = {
|
||||||
NewCommunityNoteButton(note.idHex, accountViewModel, nav)
|
NewCommunityNoteButton(note.idHex, accountViewModel, nav)
|
||||||
},
|
},
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
) {
|
) {
|
||||||
Column(
|
HorizontalPager(
|
||||||
modifier = Modifier.padding(it).consumeWindowInsets(it),
|
contentPadding = it,
|
||||||
) {
|
state = pagerState,
|
||||||
|
) { page ->
|
||||||
|
when (page) {
|
||||||
|
0 ->
|
||||||
RefresheableFeedView(
|
RefresheableFeedView(
|
||||||
feedViewModel,
|
feedViewModel,
|
||||||
null,
|
null,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
1 ->
|
||||||
|
RefresheableFeedView(
|
||||||
|
modFeedViewModel,
|
||||||
|
null,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CommunityTopBar(
|
|
||||||
id: Address,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
) {
|
|
||||||
LoadAddressableNote(id, accountViewModel) { baseNote ->
|
|
||||||
if (baseNote != null) {
|
|
||||||
TopBarExtensibleWithBackButton(
|
|
||||||
title = { ShortCommunityHeader(baseNote, accountViewModel, nav) },
|
|
||||||
extendableRow = {
|
|
||||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
|
||||||
LongCommunityHeader(baseNote = baseNote, accountViewModel = accountViewModel, nav = nav)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
popBack = nav::popBack,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(BottomTopHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -57,8 +57,8 @@ fun NewCommunityNoteButton(
|
|||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val route =
|
val route =
|
||||||
Route.NewShortNote(
|
Route.GenericCommentPost(
|
||||||
baseReplyTo = note.idHex,
|
replyTo = note.idHex,
|
||||||
)
|
)
|
||||||
nav.nav(route)
|
nav.nav(route)
|
||||||
},
|
},
|
||||||
|
|||||||
+31
-20
@@ -26,21 +26,28 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.isForCommunity
|
||||||
|
import kotlin.collections.toSet
|
||||||
|
|
||||||
class CommunityFeedFilter(
|
class CommunityFeedFilter(
|
||||||
val note: AddressableNote,
|
val communityDefNote: AddressableNote,
|
||||||
val account: Account,
|
val account: Account,
|
||||||
) : AdditiveFeedFilter<Note>() {
|
) : AdditiveFeedFilter<Note>() {
|
||||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + note.idHex
|
val communityDefEvent = communityDefNote.event as? CommunityDefinitionEvent
|
||||||
|
val moderators = communityDefEvent?.moderatorKeys()?.toSet() ?: emptySet()
|
||||||
|
|
||||||
|
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + communityDefNote.idHex
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val myPubKey = account.userProfile().pubkeyHex
|
if (communityDefEvent == null) return emptyList()
|
||||||
|
|
||||||
val result =
|
val result =
|
||||||
LocalCache.notes.mapFlattenIntoSet { _, it ->
|
LocalCache.notes.mapFlattenIntoSet { _, it ->
|
||||||
filterMap(it, myPubKey)
|
filterMap(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sort(result)
|
return sort(result)
|
||||||
@@ -49,35 +56,39 @@ class CommunityFeedFilter(
|
|||||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||||
|
|
||||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
val myPubKey = account.userProfile().pubkeyHex
|
if (communityDefEvent == null) return emptySet()
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
filterMap(it, myPubKey)
|
filterMap(it)
|
||||||
}.flatten()
|
}.flatten()
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterMap(
|
private fun filterMap(note: Note): List<Note>? {
|
||||||
note: Note,
|
val noteEvent = note.event ?: return null
|
||||||
myPubKey: HexKey,
|
|
||||||
): List<Note>? =
|
return if (
|
||||||
if (
|
|
||||||
// Only Approvals
|
// Only Approvals
|
||||||
note.event is CommunityPostApprovalEvent &&
|
(noteEvent is TextNoteEvent || noteEvent is CommentEvent || noteEvent is CommunityPostApprovalEvent) &&
|
||||||
// Of the given community
|
noteEvent.pubKey in moderators &&
|
||||||
note.event?.isTaggedAddressableNote(this.note.idHex) == true
|
noteEvent.isForCommunity(this.communityDefNote.idHex)
|
||||||
) {
|
) {
|
||||||
// if it is my post, bring on
|
if (noteEvent is CommunityPostApprovalEvent) {
|
||||||
if (note.author?.pubkeyHex == myPubKey && note.isNewThread()) {
|
note.replyTo?.filter {
|
||||||
|
it.isNewThread()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (note.isNewThread()) {
|
||||||
listOf(note)
|
listOf(note)
|
||||||
} else {
|
} else {
|
||||||
// brings the actual posts, not the approvals
|
null
|
||||||
note.replyTo?.filter { it.isNewThread() }
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||||
}
|
}
|
||||||
|
|||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* 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.communities.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.isForCommunity
|
||||||
|
|
||||||
|
class CommunityModerationFeedFilter(
|
||||||
|
val communityDefNote: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : AdditiveFeedFilter<Note>() {
|
||||||
|
val approvedFilter = CommunityFeedFilter(communityDefNote, account)
|
||||||
|
|
||||||
|
val communityDefEvent = communityDefNote.event as? CommunityDefinitionEvent
|
||||||
|
val moderators = communityDefEvent?.moderatorKeys()?.toSet() ?: emptySet()
|
||||||
|
|
||||||
|
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + communityDefNote.idHex
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
if (communityDefEvent == null) return emptyList()
|
||||||
|
|
||||||
|
val result =
|
||||||
|
LocalCache.notes.mapFlattenIntoSet { _, it ->
|
||||||
|
filterMap(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sort(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateListWith(
|
||||||
|
oldList: List<Note>,
|
||||||
|
newItems: Set<Note>,
|
||||||
|
): List<Note> {
|
||||||
|
val approved = approvedFilter.applyFilter(newItems)
|
||||||
|
|
||||||
|
val newOldList =
|
||||||
|
if (approved.isNotEmpty()) {
|
||||||
|
oldList - approved
|
||||||
|
} else {
|
||||||
|
oldList
|
||||||
|
}
|
||||||
|
|
||||||
|
val newItemsToBeAdded = applyFilter(newItems)
|
||||||
|
return if (newItemsToBeAdded.isNotEmpty()) {
|
||||||
|
val newList = newOldList.toSet() + newItemsToBeAdded
|
||||||
|
sort(newList).take(limit())
|
||||||
|
} else {
|
||||||
|
newOldList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||||
|
|
||||||
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
if (communityDefEvent == null) return emptySet()
|
||||||
|
|
||||||
|
return collection
|
||||||
|
.mapNotNull {
|
||||||
|
filterMap(it)
|
||||||
|
}.flatten()
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wasApprovedByCommunity(note: Note): Boolean =
|
||||||
|
note.boosts.any {
|
||||||
|
val approvalEvent = it.event
|
||||||
|
(approvalEvent is CommunityPostApprovalEvent || approvalEvent is RepostEvent || approvalEvent is GenericRepostEvent) &&
|
||||||
|
approvalEvent.pubKey in moderators &&
|
||||||
|
approvalEvent.isForCommunity(this.communityDefNote.idHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterMap(note: Note): List<Note>? {
|
||||||
|
val noteEvent = note.event ?: return null
|
||||||
|
|
||||||
|
return if (
|
||||||
|
(noteEvent is TextNoteEvent || noteEvent is CommentEvent || noteEvent is CommunityPostApprovalEvent) &&
|
||||||
|
noteEvent.isForCommunity(this.communityDefNote.idHex) &&
|
||||||
|
noteEvent.pubKey !in moderators
|
||||||
|
) {
|
||||||
|
if (noteEvent is CommunityPostApprovalEvent) {
|
||||||
|
note.replyTo?.filter {
|
||||||
|
it.isNewThread() && !wasApprovedByCommunity(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (note.isNewThread() && !wasApprovedByCommunity(note)) {
|
||||||
|
listOf(note)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 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.communities.dal
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||||
|
|
||||||
|
class CommunityModerationFeedViewModel(
|
||||||
|
val note: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : FeedViewModel(CommunityModerationFeedFilter(note, account)) {
|
||||||
|
class Factory(
|
||||||
|
val note: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = CommunityModerationFeedViewModel(note, account) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-6
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||||
|
|
||||||
class CommunityFeedFilterSubAssembler(
|
class CommunityFeedFilterSubAssembler(
|
||||||
client: NostrClient,
|
client: NostrClient,
|
||||||
@@ -40,13 +41,33 @@ class CommunityFeedFilterSubAssembler(
|
|||||||
return keys.flatMap {
|
return keys.flatMap {
|
||||||
val commEvent = it.community.event
|
val commEvent = it.community.event
|
||||||
if (commEvent is CommunityDefinitionEvent) {
|
if (commEvent is CommunityDefinitionEvent) {
|
||||||
val relays =
|
val communityRelays =
|
||||||
commEvent.relayUrls().ifEmpty { null }
|
(
|
||||||
?: LocalCache.relayHints.hintsForAddress(commEvent.addressTag()).ifEmpty { null }
|
commEvent.relayUrls().ifEmpty {
|
||||||
?: it.community.relayUrls()
|
LocalCache.relayHints.hintsForAddress(commEvent.addressTag())
|
||||||
|
} + it.community.relayUrls()
|
||||||
|
).toSet()
|
||||||
|
|
||||||
relays.toSet().map {
|
val moderatorsOutbox =
|
||||||
filterCommunityPosts(it, commEvent, since?.get(it)?.time)
|
mapOfSet {
|
||||||
|
commEvent.moderatorKeys().forEach { modKey ->
|
||||||
|
// hits the relay of each moderator for approvals
|
||||||
|
LocalCache.checkGetOrCreateUser(modKey)?.outboxRelays()?.forEach { relay ->
|
||||||
|
add(relay, modKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// static relay hints from this community
|
||||||
|
communityRelays.forEach { relay ->
|
||||||
|
add(relay, modKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moderatorsOutbox.map { (relay, authors) ->
|
||||||
|
filterCommunityPostsFromModerators(relay, authors, commEvent, since?.get(relay)?.time)
|
||||||
|
} +
|
||||||
|
communityRelays.map { relay ->
|
||||||
|
filterCommunityPostsFromEverybody(relay, commEvent, since?.get(relay)?.time)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
|
|||||||
+54
-2
@@ -20,11 +20,29 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
|
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||||
|
|
||||||
|
val CommunityPostKinds =
|
||||||
|
listOf(
|
||||||
|
TextNoteEvent.KIND,
|
||||||
|
CommentEvent.KIND,
|
||||||
|
RepostEvent.KIND,
|
||||||
|
GenericRepostEvent.KIND,
|
||||||
|
ClassifiedsEvent.KIND,
|
||||||
|
LongTextNoteEvent.KIND,
|
||||||
|
CommunityPostApprovalEvent.KIND,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterCommunityPosts(
|
fun filterCommunityPosts(
|
||||||
relay: NormalizedRelayUrl,
|
relay: NormalizedRelayUrl,
|
||||||
@@ -35,10 +53,44 @@ fun filterCommunityPosts(
|
|||||||
relay = relay,
|
relay = relay,
|
||||||
filter =
|
filter =
|
||||||
Filter(
|
Filter(
|
||||||
authors = community.moderators().map { it.pubKey }.plus(community.pubKey),
|
authors = community.moderatorKeys(),
|
||||||
tags = mapOf("a" to listOf(community.addressTag())),
|
tags = mapOf("a" to listOf(community.addressTag())),
|
||||||
kinds = CommunityPostApprovalEvent.KIND_LIST,
|
kinds = CommunityPostKinds,
|
||||||
limit = 500,
|
limit = 500,
|
||||||
since = since,
|
since = since,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun filterCommunityPostsFromModerators(
|
||||||
|
relay: NormalizedRelayUrl,
|
||||||
|
authors: Set<HexKey>,
|
||||||
|
community: CommunityDefinitionEvent,
|
||||||
|
since: Long?,
|
||||||
|
): RelayBasedFilter =
|
||||||
|
RelayBasedFilter(
|
||||||
|
relay = relay,
|
||||||
|
filter =
|
||||||
|
Filter(
|
||||||
|
authors = authors.sorted(),
|
||||||
|
tags = mapOf("a" to listOf(community.addressTag())),
|
||||||
|
kinds = CommunityPostKinds,
|
||||||
|
limit = 100,
|
||||||
|
since = since,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun filterCommunityPostsFromEverybody(
|
||||||
|
relay: NormalizedRelayUrl,
|
||||||
|
community: CommunityDefinitionEvent,
|
||||||
|
since: Long?,
|
||||||
|
): RelayBasedFilter =
|
||||||
|
RelayBasedFilter(
|
||||||
|
relay = relay,
|
||||||
|
filter =
|
||||||
|
Filter(
|
||||||
|
tags = mapOf("a" to listOf(community.addressTag())),
|
||||||
|
kinds = CommunityPostKinds,
|
||||||
|
limit = 100,
|
||||||
|
since = since,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
+24
-1
@@ -33,8 +33,10 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||||
@@ -50,6 +52,8 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
|||||||
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
|
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
|
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
|
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
|
||||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||||
@@ -157,7 +161,26 @@ class NotificationFeedFilter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event is BaseThreadedEvent) {
|
if (event is BaseThreadedEvent) {
|
||||||
if (note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) return true
|
if (note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((event is TextNoteEvent || event is CommentEvent)) {
|
||||||
|
val community =
|
||||||
|
event
|
||||||
|
.communityAddress()
|
||||||
|
?.let {
|
||||||
|
LocalCache.getAddressableNoteIfExists(it)
|
||||||
|
}?.event as? CommunityDefinitionEvent
|
||||||
|
if (community != null) {
|
||||||
|
val moderators = community.moderatorKeys().toSet()
|
||||||
|
val isModerator = moderators.contains(authorHex)
|
||||||
|
|
||||||
|
if (isModerator && event.pubKey !in moderators) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val isAuthoredPostCited = event.findCitations().any { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex }
|
val isAuthoredPostCited = event.findCitations().any { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex }
|
||||||
val isAuthorDirectlyCited = event.citedUsers().contains(authorHex)
|
val isAuthorDirectlyCited = event.citedUsers().contains(authorHex)
|
||||||
|
|||||||
+2
@@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
|||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||||
@@ -70,6 +71,7 @@ class UserProfileNewThreadFeedFilter(
|
|||||||
it.author == user &&
|
it.author == user &&
|
||||||
(
|
(
|
||||||
it.event is TextNoteEvent ||
|
it.event is TextNoteEvent ||
|
||||||
|
it.event is CommentEvent ||
|
||||||
it.event is ClassifiedsEvent ||
|
it.event is ClassifiedsEvent ||
|
||||||
it.event is RepostEvent ||
|
it.event is RepostEvent ||
|
||||||
it.event is GenericRepostEvent ||
|
it.event is GenericRepostEvent ||
|
||||||
|
|||||||
+55
-12
@@ -49,6 +49,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -75,6 +76,7 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
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.observeCommunityApprovalNeedStatus
|
||||||
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
||||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||||
@@ -96,7 +98,9 @@ import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction
|
|||||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.RenderApproveButton
|
||||||
import com.vitorpamplona.amethyst.ui.note.RenderDraft
|
import com.vitorpamplona.amethyst.ui.note.RenderDraft
|
||||||
import com.vitorpamplona.amethyst.ui.note.RenderRepost
|
import com.vitorpamplona.amethyst.ui.note.RenderRepost
|
||||||
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
||||||
@@ -168,6 +172,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.PaddingHorizontal12Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
@@ -189,7 +194,6 @@ import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
|||||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableKind
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
|
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
|
||||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull
|
import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull
|
||||||
@@ -227,7 +231,8 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
|||||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||||
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
|
||||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||||
@@ -435,6 +440,7 @@ private fun FullBleedNoteCompose(
|
|||||||
) {
|
) {
|
||||||
val noteEvent = baseNote.event ?: return
|
val noteEvent = baseNote.event ?: return
|
||||||
|
|
||||||
|
val isDraft = baseNote.isDraft()
|
||||||
val backgroundColor =
|
val backgroundColor =
|
||||||
calculateBackgroundColor(
|
calculateBackgroundColor(
|
||||||
baseNote.createdAt(),
|
baseNote.createdAt(),
|
||||||
@@ -467,9 +473,21 @@ private fun FullBleedNoteCompose(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f), accountViewModel = accountViewModel)
|
NoteUsernameDisplay(baseNote, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||||
|
|
||||||
val isCommunityPost =
|
if (isDraft) {
|
||||||
remember(baseNote) {
|
ObserveDraftEvent(baseNote, accountViewModel) { draftNote ->
|
||||||
baseNote.event?.isTaggedAddressableKind(CommunityDefinitionEvent.KIND) == true
|
val isCommunityPost by remember(draftNote) {
|
||||||
|
derivedStateOf { draftNote.event?.isACommunityPost() == true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCommunityPost) {
|
||||||
|
DisplayFollowingCommunityInPost(draftNote, accountViewModel, nav)
|
||||||
|
} else {
|
||||||
|
DisplayFollowingHashtagsInPost(draftNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val isCommunityPost by remember(baseNote) {
|
||||||
|
derivedStateOf { baseNote.event?.isACommunityPost() == true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCommunityPost) {
|
if (isCommunityPost) {
|
||||||
@@ -477,6 +495,7 @@ private fun FullBleedNoteCompose(
|
|||||||
} else {
|
} else {
|
||||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CheckAndDisplayEditStatus(editState)
|
CheckAndDisplayEditStatus(editState)
|
||||||
|
|
||||||
@@ -508,7 +527,7 @@ private fun FullBleedNoteCompose(
|
|||||||
DisplayPoW(pow)
|
DisplayPoW(pow)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseNote.isDraft()) {
|
if (isDraft) {
|
||||||
DisplayDraft()
|
DisplayDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,9 +546,7 @@ private fun FullBleedNoteCompose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = PaddingHorizontal12Modifier,
|
||||||
modifier
|
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
if (noteEvent is ChannelCreateEvent) {
|
if (noteEvent is ChannelCreateEvent) {
|
||||||
@@ -753,17 +770,43 @@ private fun FullBleedNoteCompose(
|
|||||||
val zapSplits = remember(noteEvent) { noteEvent.hasZapSplitSetup() }
|
val zapSplits = remember(noteEvent) { noteEvent.hasZapSplitSetup() }
|
||||||
if (zapSplits) {
|
if (zapSplits) {
|
||||||
Spacer(modifier = DoubleVertSpacer)
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
Row(
|
Row(PaddingHorizontal12Modifier) {
|
||||||
modifier = Modifier.padding(horizontal = 12.dp),
|
|
||||||
) {
|
|
||||||
DisplayZapSplits(noteEvent, false, accountViewModel, nav)
|
DisplayZapSplits(noteEvent, false, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RenderApprovalIfNeeded(baseNote, accountViewModel, nav)
|
||||||
|
|
||||||
ReactionsRow(baseNote, showReactionDetail = true, addPadding = true, editState = editState, accountViewModel = accountViewModel, nav = nav)
|
ReactionsRow(baseNote, showReactionDetail = true, addPadding = true, editState = editState, accountViewModel = accountViewModel, nav = nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderApprovalIfNeeded(
|
||||||
|
baseNote: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
if (baseNote.isNewThread()) {
|
||||||
|
val communityAddress =
|
||||||
|
remember(baseNote) {
|
||||||
|
baseNote.event?.communityAddress()
|
||||||
|
}
|
||||||
|
communityAddress?.let {
|
||||||
|
LoadAddressableNote(it, accountViewModel) { community ->
|
||||||
|
if (community != null) {
|
||||||
|
val showApproveButton by observeCommunityApprovalNeedStatus(baseNote, community, accountViewModel)
|
||||||
|
if (showApproveButton == true) {
|
||||||
|
Row(PaddingHorizontal12Modifier) {
|
||||||
|
RenderApproveButton(baseNote, community, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderClassifiedsReaderForThread(
|
private fun RenderClassifiedsReaderForThread(
|
||||||
noteEvent: ClassifiedsEvent,
|
noteEvent: ClassifiedsEvent,
|
||||||
|
|||||||
@@ -356,3 +356,5 @@ val BadgePictureModifier = Modifier.size(35.dp).clip(shape = CutCornerShape(20))
|
|||||||
val MaxWidthPaddingTop5dp = Modifier.fillMaxWidth().padding(top = 5.dp)
|
val MaxWidthPaddingTop5dp = Modifier.fillMaxWidth().padding(top = 5.dp)
|
||||||
|
|
||||||
val VoiceHeightModifier = Modifier.fillMaxWidth().height(100.dp)
|
val VoiceHeightModifier = Modifier.fillMaxWidth().height(100.dp)
|
||||||
|
|
||||||
|
val PaddingHorizontal12Modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
|||||||
@@ -175,6 +175,8 @@
|
|||||||
<string name="blocked_users">Blocked Users</string>
|
<string name="blocked_users">Blocked Users</string>
|
||||||
<string name="new_threads">New Threads</string>
|
<string name="new_threads">New Threads</string>
|
||||||
<string name="conversations">Conversations</string>
|
<string name="conversations">Conversations</string>
|
||||||
|
<string name="feed">Feed</string>
|
||||||
|
<string name="mod_queue">Mod Queue</string>
|
||||||
<string name="notes">Notes</string>
|
<string name="notes">Notes</string>
|
||||||
<string name="replies">Replies</string>
|
<string name="replies">Replies</string>
|
||||||
<string name="mutual">Yours</string>
|
<string name="mutual">Yours</string>
|
||||||
@@ -806,6 +808,10 @@
|
|||||||
<string name="created_at">Created at</string>
|
<string name="created_at">Created at</string>
|
||||||
<string name="rules">Rules</string>
|
<string name="rules">Rules</string>
|
||||||
|
|
||||||
|
<string name="community_description">About Us</string>
|
||||||
|
<string name="guidelines">Guidelines</string>
|
||||||
|
<string name="moderators">Moderators</string>
|
||||||
|
|
||||||
<string name="login_with_external_signer">Login with Amber</string>
|
<string name="login_with_external_signer">Login with Amber</string>
|
||||||
|
|
||||||
<string name="status_update">Update your status</string>
|
<string name="status_update">Update your status</string>
|
||||||
|
|||||||
+42
-40
@@ -81,50 +81,13 @@ fun TranslatableRichTextViewer(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
var translatedTextState by translateAndWatchLanguageChanges(content, id, accountViewModel)
|
TranslatableRichTextViewer(
|
||||||
|
|
||||||
CrossfadeIfEnabled(targetState = translatedTextState, accountViewModel = accountViewModel) {
|
|
||||||
RenderText(
|
|
||||||
translatedTextState = it,
|
|
||||||
content = content,
|
content = content,
|
||||||
canPreview = canPreview,
|
|
||||||
quotesLeft = quotesLeft,
|
|
||||||
modifier = modifier,
|
|
||||||
tags = tags,
|
|
||||||
backgroundColor = backgroundColor,
|
|
||||||
id = id,
|
id = id,
|
||||||
callbackUri = callbackUri,
|
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RenderText(
|
|
||||||
translatedTextState: TranslationConfig,
|
|
||||||
content: String,
|
|
||||||
canPreview: Boolean,
|
|
||||||
quotesLeft: Int,
|
|
||||||
modifier: Modifier,
|
|
||||||
tags: ImmutableListOfLists<String>,
|
|
||||||
backgroundColor: MutableState<Color>,
|
|
||||||
id: String,
|
|
||||||
callbackUri: String? = null,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
) {
|
) {
|
||||||
var showOriginal by
|
|
||||||
remember(translatedTextState) { mutableStateOf(translatedTextState.showOriginal) }
|
|
||||||
|
|
||||||
val toBeViewed by
|
|
||||||
remember(translatedTextState) {
|
|
||||||
derivedStateOf { if (showOriginal) content else translatedTextState.result ?: content }
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
ExpandableRichTextViewer(
|
ExpandableRichTextViewer(
|
||||||
toBeViewed,
|
it,
|
||||||
canPreview,
|
canPreview,
|
||||||
quotesLeft,
|
quotesLeft,
|
||||||
modifier,
|
modifier,
|
||||||
@@ -135,6 +98,45 @@ private fun RenderText(
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav,
|
nav,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TranslatableRichTextViewer(
|
||||||
|
content: String,
|
||||||
|
id: String,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
displayText: @Composable (String) -> Unit,
|
||||||
|
) {
|
||||||
|
var translatedTextState by translateAndWatchLanguageChanges(content, id, accountViewModel)
|
||||||
|
|
||||||
|
CrossfadeIfEnabled(targetState = translatedTextState, accountViewModel = accountViewModel) {
|
||||||
|
RenderTextWithTranslateOptions(
|
||||||
|
translatedTextState = it,
|
||||||
|
content = content,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
displayText = displayText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderTextWithTranslateOptions(
|
||||||
|
translatedTextState: TranslationConfig,
|
||||||
|
content: String,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
displayText: @Composable (String) -> Unit,
|
||||||
|
) {
|
||||||
|
var showOriginal by
|
||||||
|
remember(translatedTextState) { mutableStateOf(translatedTextState.showOriginal) }
|
||||||
|
|
||||||
|
val toBeViewed by
|
||||||
|
remember(translatedTextState) {
|
||||||
|
derivedStateOf { if (showOriginal) content else translatedTextState.result ?: content }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
displayText(toBeViewed)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
translatedTextState.sourceLang != null &&
|
translatedTextState.sourceLang != null &&
|
||||||
@@ -325,7 +327,7 @@ fun translateAndWatchLanguageChanges(
|
|||||||
id: String,
|
id: String,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
): MutableState<TranslationConfig> {
|
): MutableState<TranslationConfig> {
|
||||||
var translatedTextState = remember(id) { mutableStateOf(TranslationsCache.get(content)) }
|
val translatedTextState = remember(id) { mutableStateOf(TranslationsCache.get(content)) }
|
||||||
|
|
||||||
TranslateAndWatchLanguageChanges(
|
TranslateAndWatchLanguageChanges(
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ fun Event.isTaggedAddressableNotes(addressIds: Set<String>) = tags.isTaggedAddre
|
|||||||
|
|
||||||
fun Event.isTaggedAddressableKind(kind: Int) = tags.isTaggedAddressableKind(kind)
|
fun Event.isTaggedAddressableKind(kind: Int) = tags.isTaggedAddressableKind(kind)
|
||||||
|
|
||||||
|
fun Event.isTaggedAddressableKind(kindStr: String) = tags.isTaggedAddressableKind(kindStr)
|
||||||
|
|
||||||
fun Event.getTagOfAddressableKind(kind: Int) = tags.getTagOfAddressableKind(kind)
|
fun Event.getTagOfAddressableKind(kind: Int) = tags.getTagOfAddressableKind(kind)
|
||||||
|
|
||||||
|
fun Event.getTagOfAddressableKind(kindStr: String) = tags.getTagOfAddressableKind(kindStr)
|
||||||
|
|
||||||
fun Event.taggedATags() = tags.taggedATags()
|
fun Event.taggedATags() = tags.taggedATags()
|
||||||
|
|
||||||
fun Event.firstTaggedATag() = tags.firstTaggedATag()
|
fun Event.firstTaggedATag() = tags.firstTaggedATag()
|
||||||
|
|||||||
+4
@@ -35,8 +35,12 @@ fun TagArray.isTaggedAddressableNotes(addressIds: Set<String>) = this.any(ATag::
|
|||||||
|
|
||||||
fun TagArray.isTaggedAddressableKind(kind: Int) = this.any(ATag::isTaggedWithKind, kind.toString())
|
fun TagArray.isTaggedAddressableKind(kind: Int) = this.any(ATag::isTaggedWithKind, kind.toString())
|
||||||
|
|
||||||
|
fun TagArray.isTaggedAddressableKind(kindStr: String) = this.any(ATag::isTaggedWithKind, kindStr)
|
||||||
|
|
||||||
fun TagArray.getTagOfAddressableKind(kind: Int) = this.fastFirstNotNullOfOrNull(ATag::parseIfOfKind, kind.toString())
|
fun TagArray.getTagOfAddressableKind(kind: Int) = this.fastFirstNotNullOfOrNull(ATag::parseIfOfKind, kind.toString())
|
||||||
|
|
||||||
|
fun TagArray.getTagOfAddressableKind(kindStr: String) = this.fastFirstNotNullOfOrNull(ATag::parseIfOfKind, kindStr.toString())
|
||||||
|
|
||||||
fun TagArray.taggedATags() = this.mapNotNull(ATag::parse)
|
fun TagArray.taggedATags() = this.mapNotNull(ATag::parse)
|
||||||
|
|
||||||
fun TagArray.firstTaggedATag() = this.fastFirstNotNullOfOrNull(ATag::parse)
|
fun TagArray.firstTaggedATag() = this.fastFirstNotNullOfOrNull(ATag::parse)
|
||||||
|
|||||||
@@ -72,16 +72,18 @@ open class BaseThreadedEvent(
|
|||||||
return newStyleReply ?: newStyleRoot ?: oldStylePositional
|
return newStyleReply ?: newStyleRoot ?: oldStylePositional
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tagsWithoutCitations(): List<String> {
|
open fun tagsWithoutCitations(): List<String> {
|
||||||
val certainRepliesTo = markedReplyTos()
|
val certainRepliesTo = markedReplyTos()
|
||||||
val uncertainRepliesTo = unmarkedReplyTos()
|
val uncertainRepliesTo = unmarkedReplyTos()
|
||||||
|
|
||||||
val tagAddresses =
|
val tagAddresses =
|
||||||
taggedATags()
|
taggedATags()
|
||||||
.filter {
|
.filter { aTag ->
|
||||||
it.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || it.kind != WikiNoteEvent.KIND)
|
aTag.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || aTag.kind != WikiNoteEvent.KIND)
|
||||||
// removes forks from itself.
|
// removes forks from itself.
|
||||||
}.map { it.toTag() }
|
}.map {
|
||||||
|
it.toTag()
|
||||||
|
}
|
||||||
|
|
||||||
if (certainRepliesTo.isEmpty() && uncertainRepliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
|
if (certainRepliesTo.isEmpty() && uncertainRepliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.any
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||||
@@ -33,6 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
|||||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint
|
import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
|
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
|
||||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
|
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
|
||||||
@@ -55,10 +57,12 @@ import com.vitorpamplona.quartz.nip22Comments.tags.RootIdentifierTag
|
|||||||
import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag
|
import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull
|
import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull
|
||||||
|
import kotlin.collections.mapNotNull
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class CommentEvent(
|
class CommentEvent(
|
||||||
@@ -126,6 +130,28 @@ class CommentEvent(
|
|||||||
return aHints + qHints + nip19Hints
|
return aHints + qHints + nip19Hints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun rootEventIds() = tags.mapNotNull(RootEventTag::parseKey)
|
||||||
|
|
||||||
|
fun replyEventIds() = tags.mapNotNull(ReplyEventTag::parseKey)
|
||||||
|
|
||||||
|
fun rootAddressIds() = tags.mapNotNull(RootAddressTag::parseAddressId)
|
||||||
|
|
||||||
|
fun hasRootAddress(addressId: String) = tags.any(RootAddressTag::isTagged, addressId)
|
||||||
|
|
||||||
|
fun hasReplyAddress(addressId: String) = tags.any(ReplyAddressTag::isTagged, addressId)
|
||||||
|
|
||||||
|
fun replyAddressIds() = tags.mapNotNull(ReplyAddressTag::parseAddressId)
|
||||||
|
|
||||||
|
fun replyAddress() = tags.mapNotNull(ReplyAddressTag::parseAddress)
|
||||||
|
|
||||||
|
fun rootScopes() = tags.filter { RootIdentifierTag.match(it) || RootAddressTag.match(it) || RootEventTag.match(it) }
|
||||||
|
|
||||||
|
fun rootKinds() = tags.filter(RootKindTag::match)
|
||||||
|
|
||||||
|
fun directReplies() = tags.filter { ReplyIdentifierTag.match(it) || ReplyAddressTag.match(it) || ReplyEventTag.match(it) }
|
||||||
|
|
||||||
|
fun directKinds() = tags.filter(ReplyKindTag::match)
|
||||||
|
|
||||||
fun rootAuthor() = tags.firstNotNullOfOrNull(RootAuthorTag::parse)
|
fun rootAuthor() = tags.firstNotNullOfOrNull(RootAuthorTag::parse)
|
||||||
|
|
||||||
fun replyAuthor() = tags.firstNotNullOfOrNull(ReplyAuthorTag::parse)
|
fun replyAuthor() = tags.firstNotNullOfOrNull(ReplyAuthorTag::parse)
|
||||||
@@ -142,14 +168,6 @@ class CommentEvent(
|
|||||||
|
|
||||||
fun replyAuthorHints() = tags.mapNotNull(ReplyAuthorTag::parseAsHint)
|
fun replyAuthorHints() = tags.mapNotNull(ReplyAuthorTag::parseAsHint)
|
||||||
|
|
||||||
fun rootScopes() = tags.filter { RootIdentifierTag.match(it) || RootAddressTag.match(it) || RootEventTag.match(it) }
|
|
||||||
|
|
||||||
fun rootKinds() = tags.filter(RootKindTag::match)
|
|
||||||
|
|
||||||
fun directReplies() = tags.filter { ReplyIdentifierTag.match(it) || ReplyAddressTag.match(it) || ReplyEventTag.match(it) }
|
|
||||||
|
|
||||||
fun directKinds() = tags.filter(ReplyKindTag::match)
|
|
||||||
|
|
||||||
/** root and reply scope search */
|
/** root and reply scope search */
|
||||||
fun isTaggedScope(scopeId: String) = tags.any { RootIdentifierTag.isTagged(it, scopeId) || ReplyIdentifierTag.isTagged(it, scopeId) }
|
fun isTaggedScope(scopeId: String) = tags.any { RootIdentifierTag.isTagged(it, scopeId) || ReplyIdentifierTag.isTagged(it, scopeId) }
|
||||||
|
|
||||||
@@ -164,9 +182,9 @@ class CommentEvent(
|
|||||||
|
|
||||||
fun isScoped(scopeTest: (String) -> Boolean) = tags.any { RootIdentifierTag.isTagged(it, scopeTest) || ReplyIdentifierTag.isTagged(it, scopeTest) }
|
fun isScoped(scopeTest: (String) -> Boolean) = tags.any { RootIdentifierTag.isTagged(it, scopeTest) || ReplyIdentifierTag.isTagged(it, scopeTest) }
|
||||||
|
|
||||||
fun hasRootScopeKind(kind: String) = tags.any { RootKindTag.isKind(it, kind) }
|
fun hasRootScopeKind(kind: String) = tags.any(RootKindTag::isKind, kind)
|
||||||
|
|
||||||
fun hasReplyScopeKind(kind: String) = tags.any { ReplyKindTag.isKind(it, kind) }
|
fun hasReplyScopeKind(kind: String) = tags.any(ReplyKindTag::isKind, kind)
|
||||||
|
|
||||||
fun hasScopeKind(kind: String) = tags.any { RootKindTag.isKind(it, kind) || ReplyKindTag.isKind(it, kind) }
|
fun hasScopeKind(kind: String) = tags.any { RootKindTag.isKind(it, kind) || ReplyKindTag.isKind(it, kind) }
|
||||||
|
|
||||||
@@ -184,12 +202,31 @@ class CommentEvent(
|
|||||||
tags.lastNotNullOfOrNull(ReplyEventTag::parseKey)
|
tags.lastNotNullOfOrNull(ReplyEventTag::parseKey)
|
||||||
?: tags.lastNotNullOfOrNull(RootEventTag::parseKey)
|
?: tags.lastNotNullOfOrNull(RootEventTag::parseKey)
|
||||||
|
|
||||||
|
fun rootAddress() = tags.mapNotNull(RootAddressTag::parseAddress)
|
||||||
|
|
||||||
|
fun rootAddressId() = tags.mapNotNull(RootAddressTag::parseAddressId)
|
||||||
|
|
||||||
fun replyingToAddressId(): String? =
|
fun replyingToAddressId(): String? =
|
||||||
tags.lastNotNullOfOrNull(RootAddressTag::parseAddressId)
|
tags.lastNotNullOfOrNull(ReplyAddressTag::parseAddressId)
|
||||||
?: tags.lastNotNullOfOrNull(ReplyAddressTag::parseAddressId)
|
?: tags.lastNotNullOfOrNull(RootAddressTag::parseAddressId)
|
||||||
|
|
||||||
override fun replyingToAddressOrEvent(): HexKey? = replyingToAddressId() ?: replyingTo()
|
override fun replyingToAddressOrEvent(): HexKey? = replyingToAddressId() ?: replyingTo()
|
||||||
|
|
||||||
|
override fun tagsWithoutCitations(): List<String> {
|
||||||
|
val rootAddress = rootAddressIds()
|
||||||
|
val replyAddress = replyAddressIds()
|
||||||
|
|
||||||
|
if (
|
||||||
|
rootAddress.any { Address.isOfKind(it, CommunityDefinitionEvent.KIND_STR) } &&
|
||||||
|
replyAddress.any { Address.isOfKind(it, CommunityDefinitionEvent.KIND_STR) }
|
||||||
|
) {
|
||||||
|
// this is the root of the community post
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootAddress + replyAddress + rootEventIds() + replyEventIds()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 1111
|
const val KIND = 1111
|
||||||
const val ALT = "Reply to "
|
const val ALT = "Reply to "
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ class ReplyAddressTag(
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isTagged(
|
||||||
|
tag: Array<String>,
|
||||||
|
addressId: String,
|
||||||
|
) = tag.has(1) && tag[0] == RootAddressTag.Companion.TAG_NAME && tag[1] == addressId
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parse(tag: Array<String>): ReplyAddressTag? {
|
fun parse(tag: Array<String>): ReplyAddressTag? {
|
||||||
ensure(tag.has(1)) { return null }
|
ensure(tag.has(1)) { return null }
|
||||||
@@ -55,6 +61,15 @@ class ReplyAddressTag(
|
|||||||
return ReplyAddressTag(tag[1], relayHint)
|
return ReplyAddressTag(tag[1], relayHint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseAddress(tag: Array<String>): Address? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
|
||||||
|
return Address.parse(tag[1])
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parseAddressId(tag: Array<String>): String? {
|
fun parseAddressId(tag: Array<String>): String? {
|
||||||
ensure(tag.has(1)) { return null }
|
ensure(tag.has(1)) { return null }
|
||||||
|
|||||||
@@ -67,6 +67,15 @@ class RootAddressTag(
|
|||||||
return RootAddressTag(tag[1], relayHint)
|
return RootAddressTag(tag[1], relayHint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseAddress(tag: Array<String>): Address? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
|
||||||
|
return Address.parse(tag[1])
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parseValidAddress(tag: Array<String>): String? {
|
fun parseValidAddress(tag: Array<String>): String? {
|
||||||
ensure(tag.has(1)) { return null }
|
ensure(tag.has(1)) { return null }
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* 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.quartz.nip72ModCommunities
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.any
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
|
||||||
|
|
||||||
|
fun Event.isACommunityPost(): Boolean =
|
||||||
|
if (this is CommentEvent) {
|
||||||
|
this.isCommunityScoped()
|
||||||
|
} else {
|
||||||
|
this.tags.any(CommunityTag::isTaggedWithKind, CommunityDefinitionEvent.KIND_STR)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Event.isForCommunity(communityAddressId: String): Boolean =
|
||||||
|
if (this is CommentEvent) {
|
||||||
|
this.hasRootAddress(communityAddressId)
|
||||||
|
} else {
|
||||||
|
tags.any(CommunityTag::isTagged, communityAddressId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Event.communityAddress(): Address? =
|
||||||
|
if (this is CommentEvent) {
|
||||||
|
this.rootAddress().firstNotNullOfOrNull {
|
||||||
|
if (it.kind == CommunityDefinitionEvent.KIND) it else null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.tags.fastFirstNotNullOfOrNull(CommunityTag::parseAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CommentEvent.isCommunityScoped() = hasRootScopeKind(CommunityDefinitionEvent.KIND_STR)
|
||||||
|
|
||||||
|
fun CommentEvent.communityScope() = this.tags.fastFirstNotNullOfOrNull(CommunityTag::parseAddress)
|
||||||
|
|
||||||
|
fun CommentEvent.communityScopes() = this.tags.mapNotNull(CommunityTag::parseAddress)
|
||||||
+28
-15
@@ -25,22 +25,29 @@ import androidx.compose.runtime.Immutable
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.any
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.isTagged
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
|
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
|
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag.Companion.parseAddress
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag.Companion.parseAddressId
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag.Companion.parseAsHint
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedEventTag
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedEventTag.Companion.parse
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedEventTag.Companion.parseAsHint
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedEventTag.Companion.parseId
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import kotlin.collections.mapNotNull
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class CommunityPostApprovalEvent(
|
class CommunityPostApprovalEvent(
|
||||||
@@ -54,13 +61,17 @@ class CommunityPostApprovalEvent(
|
|||||||
EventHintProvider,
|
EventHintProvider,
|
||||||
AddressHintProvider,
|
AddressHintProvider,
|
||||||
PubKeyHintProvider {
|
PubKeyHintProvider {
|
||||||
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint)
|
override fun eventHints() = tags.mapNotNull(ApprovedEventTag::parseAsHint)
|
||||||
|
|
||||||
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + tags.mapNotNull(QTag::parseEventId)
|
override fun linkedEventIds() = tags.mapNotNull(ApprovedEventTag::parseId)
|
||||||
|
|
||||||
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint)
|
override fun addressHints() =
|
||||||
|
tags.mapNotNull(ApprovedAddressTag::parseAsHint) +
|
||||||
|
tags.mapNotNull(CommunityTag::parseAsHint)
|
||||||
|
|
||||||
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + tags.mapNotNull(QTag::parseAddressId)
|
override fun linkedAddressIds() =
|
||||||
|
tags.mapNotNull(ApprovedAddressTag::parseAddressId) +
|
||||||
|
tags.mapNotNull(CommunityTag::parseAddressId)
|
||||||
|
|
||||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||||
|
|
||||||
@@ -77,15 +88,17 @@ class CommunityPostApprovalEvent(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun communities() = taggedATags().filter { it.kind == CommunityDefinitionEvent.KIND }
|
fun isForCommunity(communityAddressId: String): Boolean = tags.any(CommunityTag::isTagged, communityAddressId)
|
||||||
|
|
||||||
fun communityAddresses() = taggedAddresses().filter { it.kind == CommunityDefinitionEvent.KIND }
|
fun isForCommunity(community: Address): Boolean = isForCommunity(community.toValue())
|
||||||
|
|
||||||
fun approvedEvents() = taggedEvents()
|
fun communities(): List<CommunityTag> = tags.mapNotNull(CommunityTag::parse)
|
||||||
|
|
||||||
fun approvedATags() = taggedATags().filter { it.kind != CommunityDefinitionEvent.KIND }
|
fun communityAddresses(): List<Address> = tags.mapNotNull(CommunityTag::parseAddress)
|
||||||
|
|
||||||
fun approvedAddresses() = taggedAddresses().filter { it.kind != CommunityDefinitionEvent.KIND }
|
fun approvedEvents(): List<HexKey> = tags.mapNotNull(ApprovedEventTag::parseId)
|
||||||
|
|
||||||
|
fun approvedAddresses(): List<Address> = tags.mapNotNull(ApprovedAddressTag::parseAddress)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 4550
|
const val KIND = 4550
|
||||||
|
|||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* 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.quartz.nip72ModCommunities.approval.tags
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||||
|
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||||
|
|
||||||
|
class ApprovedAddressTag(
|
||||||
|
val address: Address,
|
||||||
|
val relayHint: NormalizedRelayUrl? = null,
|
||||||
|
) {
|
||||||
|
fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
|
||||||
|
|
||||||
|
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
|
||||||
|
|
||||||
|
fun toTagArray() = assemble(address, relayHint)
|
||||||
|
|
||||||
|
fun toTagIdOnly() = assemble(address, null)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG_NAME = "a"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && !Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isTagged(
|
||||||
|
tag: Array<String>,
|
||||||
|
addressId: String,
|
||||||
|
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == addressId
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isTagged(
|
||||||
|
tag: Array<String>,
|
||||||
|
address: ApprovedAddressTag,
|
||||||
|
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isIn(
|
||||||
|
tag: Array<String>,
|
||||||
|
addressIds: Set<String>,
|
||||||
|
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parse(tag: Array<String>): ApprovedAddressTag? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||||
|
|
||||||
|
val address = Address.parse(tag[1]) ?: return null
|
||||||
|
val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||||
|
return ApprovedAddressTag(address, relayHint)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseValidAddress(tag: Array<String>): String? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||||
|
return Address.parse(tag[1])?.toValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseAddress(tag: Array<String>): Address? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
val address = Address.parse(tag[1]) ?: return null
|
||||||
|
ensure(address.kind != CommunityDefinitionEvent.KIND) { return null }
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseAddressId(tag: Array<String>): String? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||||
|
return tag[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseAsHint(tag: Array<String>): AddressHint? {
|
||||||
|
ensure(tag.has(2)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||||
|
ensure(tag[1].contains(':')) { return null }
|
||||||
|
ensure(tag[2].isNotEmpty()) { return null }
|
||||||
|
|
||||||
|
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||||
|
ensure(relayHint != null) { return null }
|
||||||
|
|
||||||
|
return AddressHint(tag[1], relayHint)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun assemble(
|
||||||
|
aTagId: HexKey,
|
||||||
|
relay: NormalizedRelayUrl?,
|
||||||
|
) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun assemble(
|
||||||
|
address: Address,
|
||||||
|
relay: NormalizedRelayUrl?,
|
||||||
|
) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun assemble(
|
||||||
|
kind: Int,
|
||||||
|
pubKey: String,
|
||||||
|
dTag: String,
|
||||||
|
relay: NormalizedRelayUrl?,
|
||||||
|
) = assemble(Address.assemble(kind, pubKey, dTag), relay)
|
||||||
|
}
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* 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.quartz.nip72ModCommunities.approval.tags
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference
|
||||||
|
import com.vitorpamplona.quartz.utils.Hex
|
||||||
|
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class ApprovedEventTag(
|
||||||
|
val ref: EventReference,
|
||||||
|
) {
|
||||||
|
constructor(eventId: String, relayHint: NormalizedRelayUrl?, pubkey: String?) : this(EventReference(eventId, pubkey, relayHint))
|
||||||
|
|
||||||
|
fun toTagArray() = assemble(ref)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG_NAME = "e"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isTagged(
|
||||||
|
tag: Array<String>,
|
||||||
|
eventId: String,
|
||||||
|
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == eventId
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isIn(
|
||||||
|
tag: Array<String>,
|
||||||
|
eventIds: Set<String>,
|
||||||
|
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in eventIds
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parse(tag: Array<String>): ApprovedEventTag? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].length == 64) { return null }
|
||||||
|
return ApprovedEventTag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, tag.getOrNull(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseId(tag: Array<String>): String? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].length == 64) { return null }
|
||||||
|
return tag[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseValidId(tag: Array<String>): String? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].length == 64) { return null }
|
||||||
|
ensure(Hex.isHex(tag[1])) { return null }
|
||||||
|
return tag[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseAsHint(tag: Array<String>): EventIdHint? {
|
||||||
|
ensure(tag.has(2)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].length == 64) { return null }
|
||||||
|
ensure(tag[2].isNotEmpty()) { return null }
|
||||||
|
|
||||||
|
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||||
|
ensure(relayHint != null) { return null }
|
||||||
|
|
||||||
|
return EventIdHint(tag[1], relayHint)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun assemble(
|
||||||
|
eventId: HexKey,
|
||||||
|
relay: NormalizedRelayUrl?,
|
||||||
|
pubkey: String?,
|
||||||
|
) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, pubkey)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun assemble(ref: EventReference) = assemble(ref.eventId, ref.relayHint, ref.author)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -80,7 +80,7 @@ class CommunityDefinitionEvent(
|
|||||||
|
|
||||||
fun moderators() = tags.mapNotNull(ModeratorTag::parse)
|
fun moderators() = tags.mapNotNull(ModeratorTag::parse)
|
||||||
|
|
||||||
fun moderatorKeys() = tags.mapNotNull(ModeratorTag::parseKey)
|
fun moderatorKeys() = tags.mapNotNull(ModeratorTag::parseKey).plus(pubKey)
|
||||||
|
|
||||||
fun relays() = tags.mapNotNull(RelayTag::parse)
|
fun relays() = tags.mapNotNull(RelayTag::parse)
|
||||||
|
|
||||||
@@ -88,6 +88,7 @@ class CommunityDefinitionEvent(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 34550
|
const val KIND = 34550
|
||||||
|
const val KIND_STR = "34550"
|
||||||
const val ALT_DESCRIPTION = "Community definition"
|
const val ALT_DESCRIPTION = "Community definition"
|
||||||
|
|
||||||
fun build(
|
fun build(
|
||||||
|
|||||||
+17
-20
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||||
import com.vitorpamplona.quartz.utils.ensure
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
@@ -47,7 +48,7 @@ class CommunityTag(
|
|||||||
const val TAG_NAME = "a"
|
const val TAG_NAME = "a"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isTagged(
|
fun isTagged(
|
||||||
@@ -61,12 +62,6 @@ class CommunityTag(
|
|||||||
address: CommunityTag,
|
address: CommunityTag,
|
||||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag()
|
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag()
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun isIn(
|
|
||||||
tag: Array<String>,
|
|
||||||
addressIds: Set<String>,
|
|
||||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isTaggedWithKind(
|
fun isTaggedWithKind(
|
||||||
tag: Array<String>,
|
tag: Array<String>,
|
||||||
@@ -74,26 +69,27 @@ class CommunityTag(
|
|||||||
) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], kind)
|
) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], kind)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parse(
|
fun isIn(
|
||||||
aTagId: String,
|
tag: Array<String>,
|
||||||
relay: String?,
|
addressIds: Set<String>,
|
||||||
) = Address.parse(aTagId)?.let {
|
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
|
||||||
CommunityTag(it, relay?.let { RelayUrlNormalizer.normalizeOrNull(it) })
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parse(tag: Array<String>): CommunityTag? {
|
fun parse(tag: Array<String>): CommunityTag? {
|
||||||
ensure(tag.has(1)) { return null }
|
ensure(tag.has(1)) { return null }
|
||||||
ensure(tag[0] == TAG_NAME) { return null }
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
ensure(tag[1].isNotEmpty()) { return null }
|
ensure(Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||||
return parse(tag[1], tag.getOrNull(2))
|
|
||||||
|
val address = Address.parse(tag[1]) ?: return null
|
||||||
|
val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||||
|
return CommunityTag(address, relayHint)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parseValidAddress(tag: Array<String>): String? {
|
fun parseValidAddress(tag: Array<String>): String? {
|
||||||
ensure(tag.has(1)) { return null }
|
ensure(tag.has(1)) { return null }
|
||||||
ensure(tag[0] == TAG_NAME) { return null }
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
ensure(tag[1].isNotEmpty()) { return null }
|
ensure(Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||||
return Address.parse(tag[1])?.toValue()
|
return Address.parse(tag[1])?.toValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,14 +98,16 @@ class CommunityTag(
|
|||||||
ensure(tag.has(1)) { return null }
|
ensure(tag.has(1)) { return null }
|
||||||
ensure(tag[0] == TAG_NAME) { return null }
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
ensure(tag[1].isNotEmpty()) { return null }
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
return Address.parse(tag[1])
|
val address = Address.parse(tag[1]) ?: return null
|
||||||
|
ensure(address.kind == CommunityDefinitionEvent.KIND) { return null }
|
||||||
|
return address
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parseAddressId(tag: Array<String>): String? {
|
fun parseAddressId(tag: Array<String>): String? {
|
||||||
ensure(tag.has(1)) { return null }
|
ensure(tag.has(1)) { return null }
|
||||||
ensure(tag[0] == TAG_NAME) { return null }
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
ensure(tag[1].isNotEmpty()) { return null }
|
ensure(Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||||
return tag[1]
|
return tag[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +115,7 @@ class CommunityTag(
|
|||||||
fun parseAsHint(tag: Array<String>): AddressHint? {
|
fun parseAsHint(tag: Array<String>): AddressHint? {
|
||||||
ensure(tag.has(2)) { return null }
|
ensure(tag.has(2)) { return null }
|
||||||
ensure(tag[0] == TAG_NAME) { return null }
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
ensure(tag[1].isNotEmpty()) { return null }
|
ensure(Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||||
ensure(tag[1].contains(':')) { return null }
|
|
||||||
ensure(tag[2].isNotEmpty()) { return null }
|
ensure(tag[2].isNotEmpty()) { return null }
|
||||||
|
|
||||||
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
|
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||||
|
|||||||
Reference in New Issue
Block a user