- 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.VideoNormalEvent
|
||||
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.nip92IMeta.IMetaTag
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
@@ -863,6 +865,24 @@ class Account(
|
||||
|
||||
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(event: Event?) {
|
||||
|
||||
@@ -1453,6 +1453,8 @@ object LocalCache : ILocalCache {
|
||||
// Counts the replies
|
||||
repliesTo.forEach { it.addBoost(note) }
|
||||
|
||||
eventsApproved.forEach { it.addBoost(note) }
|
||||
|
||||
event.containedPost()?.let {
|
||||
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.NEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||
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.nip59Giftwrap.WrappedEvent
|
||||
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.anyAsync
|
||||
import com.vitorpamplona.quartz.utils.containsAny
|
||||
@@ -201,6 +203,16 @@ open class Note(
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
return if (relays.isNotEmpty()) {
|
||||
|
||||
@@ -36,12 +36,14 @@ class ThreadAssembler {
|
||||
): 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)
|
||||
|
||||
val markedAsRoot =
|
||||
note.event
|
||||
noteEvent
|
||||
?.tags
|
||||
?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }
|
||||
?.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
|
||||
|
||||
// recursive
|
||||
|
||||
+51
@@ -29,6 +29,11 @@ import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
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.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -356,3 +361,49 @@ fun observeNoteEdits(
|
||||
.stateFlow
|
||||
.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.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
@@ -45,6 +46,7 @@ val RepliesAndReactionsToAddressesKinds1 =
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
)
|
||||
|
||||
val PostsAndChatMessagesToAddresses =
|
||||
|
||||
+4
-2
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
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.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
@@ -51,7 +52,7 @@ val RepliesAndReactionsKinds =
|
||||
PollNoteEvent.KIND,
|
||||
OtsEvent.KIND,
|
||||
TextNoteModificationEvent.KIND,
|
||||
GitReplyEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
)
|
||||
|
||||
val RepliesAndReactionsKinds2 =
|
||||
@@ -60,6 +61,7 @@ val RepliesAndReactionsKinds2 =
|
||||
NIP90ContentDiscoveryResponseEvent.KIND,
|
||||
NIP90StatusEvent.KIND,
|
||||
TorrentCommentEvent.KIND,
|
||||
GitReplyEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterRepliesAndReactionsToNotes(
|
||||
@@ -108,7 +110,7 @@ fun filterRepliesAndReactionsToNotes(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(TextNoteEvent.KIND),
|
||||
kinds = listOf(TextNoteEvent.KIND, CommentEvent.KIND),
|
||||
tags = mapOf("q" to sortedList),
|
||||
since = since,
|
||||
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.theme.ButtonBorder
|
||||
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.nip36SensitiveContent.isSensitiveOrNSFW
|
||||
|
||||
@@ -113,7 +114,7 @@ fun SensitivityWarning(
|
||||
@Composable
|
||||
fun ContentWarningNote(onDismiss: () -> Unit) {
|
||||
Column {
|
||||
Row(modifier = Modifier.padding(horizontal = 12.dp)) {
|
||||
Row(modifier = PaddingHorizontal12Modifier) {
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
Box(
|
||||
|
||||
@@ -29,7 +29,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -49,8 +48,6 @@ fun FeedLoaded(
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
|
||||
+9
-1
@@ -64,9 +64,10 @@ fun DisappearingScaffold(
|
||||
bottomBar: (@Composable () -> Unit)? = null,
|
||||
floatingButton: (@Composable () -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
isActive: () -> Boolean = { true },
|
||||
mainContent: @Composable (padding: PaddingValues) -> Unit,
|
||||
) {
|
||||
val shouldShow = remember { mutableStateOf(true) }
|
||||
val shouldShow = remember { mutableStateOf(isActive()) }
|
||||
|
||||
val modifier =
|
||||
if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) {
|
||||
@@ -80,6 +81,13 @@ fun DisappearingScaffold(
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset {
|
||||
if (!isActive()) {
|
||||
if (!shouldShow.value) {
|
||||
shouldShow.value = true
|
||||
}
|
||||
return Offset.Zero
|
||||
}
|
||||
|
||||
val newOffset = bottomBarOffsetHeightPx.floatValue + available.y
|
||||
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.nip28PublicChats.PublicChatChannel
|
||||
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.observeNoteEvent
|
||||
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.RowColSpacing10dp
|
||||
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.Size30dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
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.publicMessages.PublicMessageEvent
|
||||
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.nip04Dm.messages.PrivateDmEvent
|
||||
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.VideoVerticalEvent
|
||||
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.isACommunityPost
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||
@@ -552,6 +558,8 @@ fun InnerNoteWithReactions(
|
||||
accountViewModel = accountViewModel,
|
||||
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
|
||||
fun NoteBody(
|
||||
baseNote: Note,
|
||||
@@ -1090,14 +1139,7 @@ fun FirstUserInfoRow(
|
||||
) {
|
||||
Row(verticalAlignment = CenterVertically, modifier = UserNameRowHeight) {
|
||||
val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
||||
|
||||
val isCommunityPost by
|
||||
remember(baseNote) {
|
||||
derivedStateOf {
|
||||
baseNote.event?.isTaggedAddressableKind(CommunityDefinitionEvent.KIND) == true
|
||||
}
|
||||
}
|
||||
|
||||
val isDraft = baseNote.isDraft()
|
||||
val textColor = if (isRepost) MaterialTheme.colorScheme.grayText else Color.Unspecified
|
||||
|
||||
if (showAuthorPicture) {
|
||||
@@ -1108,10 +1150,28 @@ fun FirstUserInfoRow(
|
||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
if (isCommunityPost) {
|
||||
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
|
||||
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 {
|
||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||
val isCommunityPost by remember(baseNote) {
|
||||
derivedStateOf { baseNote.event?.isACommunityPost() == true }
|
||||
}
|
||||
|
||||
if (isCommunityPost) {
|
||||
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
if (isRepost) {
|
||||
@@ -1120,7 +1180,7 @@ fun FirstUserInfoRow(
|
||||
|
||||
CheckAndDisplayEditStatus(editState)
|
||||
|
||||
if (baseNote.isDraft()) {
|
||||
if (isDraft) {
|
||||
DisplayDraft()
|
||||
}
|
||||
|
||||
@@ -1192,7 +1252,7 @@ fun observeEdits(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BadgeBox(
|
||||
fun BadgeBox(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
@@ -1207,7 +1267,7 @@ private fun BadgeBox(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderAuthorImages(
|
||||
fun RenderAuthorImages(
|
||||
baseNote: Note,
|
||||
nav: INav,
|
||||
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.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.getTagOfAddressableKind
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||
|
||||
@Composable
|
||||
fun DisplayFollowingCommunityInPost(
|
||||
@@ -55,11 +54,15 @@ private fun DisplayCommunity(
|
||||
nav: INav,
|
||||
) {
|
||||
val communityTag =
|
||||
remember(note) { note.event?.getTagOfAddressableKind(CommunityDefinitionEvent.KIND) } ?: return
|
||||
remember(note) { note.event?.communityAddress() } ?: return
|
||||
|
||||
val displayTag =
|
||||
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(
|
||||
@@ -75,12 +78,12 @@ private fun DisplayCommunity(
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCommunityShortName(communityTag: ATag): String {
|
||||
fun getCommunityShortName(communityAddress: Address): String {
|
||||
val name =
|
||||
if (communityTag.dTag.length > 10) {
|
||||
communityTag.dTag.take(10) + "..."
|
||||
if (communityAddress.dTag.length > 10) {
|
||||
communityAddress.dTag.take(10) + "..."
|
||||
} else {
|
||||
communityTag.dTag.take(10)
|
||||
communityAddress.dTag.take(10)
|
||||
}
|
||||
|
||||
return "/n/$name"
|
||||
|
||||
+27
-5
@@ -86,6 +86,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||
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.scope
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
||||
@@ -252,8 +253,7 @@ open class CommentPostViewModel :
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: CommentEvent) {
|
||||
val scope = draftEvent.scope() ?: return
|
||||
this.externalIdentity = scope
|
||||
this.externalIdentity = draftEvent.scope()
|
||||
|
||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
@@ -278,8 +278,14 @@ open class CommentPostViewModel :
|
||||
zapRaiserAmount.value = zapraiser
|
||||
}
|
||||
|
||||
draftEvent.replyingTo()?.let {
|
||||
replyingTo = LocalCache.getOrCreateNote(it)
|
||||
val replyAddress = draftEvent.replyAddress()
|
||||
|
||||
if (replyAddress.isNotEmpty()) {
|
||||
replyingTo = LocalCache.getOrCreateAddressableNote(replyAddress.first())
|
||||
} else {
|
||||
draftEvent.replyingTo()?.let {
|
||||
replyingTo = LocalCache.getOrCreateNote(it)
|
||||
}
|
||||
}
|
||||
|
||||
wantsToAddGeoHash = draftEvent.hasGeohashes()
|
||||
@@ -356,6 +362,7 @@ open class CommentPostViewModel :
|
||||
val contentWarningReason = if (wantsToMarkAsSensitive) "" else null
|
||||
|
||||
val replyingTo = replyingTo
|
||||
val replyingToEvent = replyingTo?.event
|
||||
|
||||
val template =
|
||||
if (replyingTo != null) {
|
||||
@@ -365,7 +372,22 @@ open class CommentPostViewModel :
|
||||
msg = tagger.message,
|
||||
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))
|
||||
references(findURLs(tagger.message))
|
||||
|
||||
+257
-127
@@ -20,16 +20,27 @@
|
||||
*/
|
||||
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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -37,14 +48,18 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.User
|
||||
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.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
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.UsernameDisplay
|
||||
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.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
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.RowColSpacing
|
||||
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.Size35dp
|
||||
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.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
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.tags.ModeratorTag
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.Locale
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
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
|
||||
fun LongCommunityHeader(
|
||||
baseNote: AddressableNote,
|
||||
@@ -118,142 +148,169 @@ fun LongCommunityHeader(
|
||||
nav: INav,
|
||||
) {
|
||||
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 }
|
||||
val defaultBackground = MaterialTheme.colorScheme.background
|
||||
val background = remember { mutableStateOf(defaultBackground) }
|
||||
|
||||
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"
|
||||
}
|
||||
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(
|
||||
content = description ?: stringRes(id = R.string.community_no_descriptor),
|
||||
id = baseNote.idHex + "description",
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
RichTextViewer(
|
||||
it,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
canPreview = false,
|
||||
quotesLeft = 1,
|
||||
tags = tagList,
|
||||
backgroundColor = background,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.weight(1f),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val defaultBackground = MaterialTheme.colorScheme.background
|
||||
val background = remember { mutableStateOf(defaultBackground) }
|
||||
guidelines?.let {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Title(title = stringRes(R.string.guidelines))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = summary ?: stringRes(id = R.string.community_no_descriptor),
|
||||
canPreview = false,
|
||||
quotesLeft = 1,
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = background,
|
||||
id = baseNote.idHex,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
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 {
|
||||
if (it.hasHashtags()) {
|
||||
DisplayUncitedHashtags(
|
||||
event = it,
|
||||
content = summary ?: "",
|
||||
content = description ?: "",
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Title(title = stringRes(R.string.owner))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NoteAuthorPicture(baseNote, Size25dp, accountViewModel = accountViewModel, nav = nav)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
LongCommunityActionOptions(baseNote, accountViewModel, nav)
|
||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
lineModifier,
|
||||
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)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||
}
|
||||
var participantUsers by
|
||||
remember(baseNote) {
|
||||
mutableStateOf<ImmutableList<Pair<ModeratorTag, User>>>(
|
||||
persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
var participantUsers by
|
||||
remember(baseNote) {
|
||||
mutableStateOf<ImmutableList<Pair<ModeratorTag, User>>>(
|
||||
persistentListOf(),
|
||||
)
|
||||
}
|
||||
LaunchedEffect(key1 = noteEvent) {
|
||||
val participants = noteEvent?.moderators()
|
||||
|
||||
LaunchedEffect(key1 = noteEvent) {
|
||||
val participants = noteEvent?.moderators()
|
||||
|
||||
if (participants != null) {
|
||||
accountViewModel.loadParticipants(participants) { newParticipantUsers ->
|
||||
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||
participantUsers = newParticipantUsers
|
||||
if (participants != null) {
|
||||
accountViewModel.loadParticipants(participants) { newParticipantUsers ->
|
||||
val noOwner = newParticipantUsers.filter { it.second != baseNote.author }.toImmutableList()
|
||||
if (!equalImmutableLists(noOwner, participantUsers)) {
|
||||
participantUsers = noOwner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
participantUsers.forEach {
|
||||
Row(
|
||||
lineModifier.clickable { nav.nav(routeFor(it.second)) },
|
||||
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),
|
||||
)
|
||||
if (participantUsers.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Title(title = stringRes(R.string.moderators))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
participantUsers.forEach {
|
||||
Column(Modifier.padding(vertical = 5.dp).clickable { nav.nav(routeFor(it.second)) }) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ClickableUserPicture(it.second, Size25dp, accountViewModel)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
UsernameDisplay(it.second, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(DoubleHorzSpacer)
|
||||
ClickableUserPicture(it.second, Size25dp, accountViewModel)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
UsernameDisplay(it.second, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
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)
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +365,47 @@ fun ShortCommunityHeader(
|
||||
}
|
||||
|
||||
@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,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
@@ -346,9 +443,12 @@ private fun LongCommunityActionOptions(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
|
||||
if (isFollowing) {
|
||||
LeaveCommunityButton(accountViewModel, note, nav)
|
||||
Row {
|
||||
ShareCommunityButton(accountViewModel, note, nav)
|
||||
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
|
||||
if (isFollowing) {
|
||||
LeaveCommunityButton(accountViewModel, note, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,18 +488,48 @@ fun LeaveCommunityButton(
|
||||
note: AddressableNote,
|
||||
nav: INav,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Button(
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = { accountViewModel.unfollow(note) },
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
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.FeedEmpty
|
||||
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.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
@@ -102,8 +103,7 @@ fun RenderFeedState(
|
||||
nav: INav,
|
||||
routeForLastRead: String?,
|
||||
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() } },
|
||||
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.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
@@ -123,36 +124,40 @@ class FollowListState(
|
||||
|
||||
val livePeopleListsFlow = MutableStateFlow(emptyList<FeedDefinition>())
|
||||
|
||||
fun hasItemInNoteList(notes: Set<Note>): Boolean =
|
||||
notes.any { it ->
|
||||
val noteEvent = it.event
|
||||
|
||||
noteEvent?.pubKey == account.userProfile().pubkeyHex &&
|
||||
(
|
||||
(
|
||||
noteEvent is PeopleListEvent ||
|
||||
noteEvent is FollowListEvent ||
|
||||
noteEvent is MuteListEvent ||
|
||||
noteEvent is ContactListEvent
|
||||
) ||
|
||||
(
|
||||
noteEvent is DeletionEvent &&
|
||||
(
|
||||
noteEvent.deleteEventIds().any { LocalCache.getNoteIfExists(it)?.event is PeopleListEvent } ||
|
||||
noteEvent.deleteAddresses().any { it.kind == PeopleListEvent.KIND }
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteFromFeed(deletedNotes: Set<Note>) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (hasItemInNoteList(deletedNotes)) {
|
||||
livePeopleListsFlow.tryEmit(getPeopleLists())
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFeedWith(newNotes: Set<Note>) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val hasNewList =
|
||||
newNotes.any { it ->
|
||||
val noteEvent = it.event
|
||||
|
||||
noteEvent?.pubKey == account.userProfile().pubkeyHex &&
|
||||
(
|
||||
(
|
||||
noteEvent is PeopleListEvent ||
|
||||
noteEvent is FollowListEvent ||
|
||||
noteEvent is MuteListEvent ||
|
||||
noteEvent is ContactListEvent
|
||||
) ||
|
||||
(
|
||||
noteEvent is DeletionEvent &&
|
||||
(
|
||||
noteEvent.deleteEventIds().any {
|
||||
LocalCache.getNoteIfExists(it)?.event is PeopleListEvent
|
||||
} ||
|
||||
noteEvent.deleteAddresses().any {
|
||||
it.kind == PeopleListEvent.KIND
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (hasNewList) {
|
||||
if (hasItemInNoteList(newNotes)) {
|
||||
livePeopleListsFlow.tryEmit(getPeopleLists())
|
||||
}
|
||||
}
|
||||
@@ -163,7 +168,7 @@ class FollowListState(
|
||||
checkNotInMainThread()
|
||||
|
||||
val communities =
|
||||
communityList.mapNotNull {
|
||||
communityList.map {
|
||||
LocalCache.getOrCreateAddressableNote(it.address).let { communityNote ->
|
||||
TagFeedDefinition(
|
||||
"Community/${communityNote.idHex}",
|
||||
@@ -377,6 +382,7 @@ val DEFAULT_FEED_KINDS =
|
||||
val DEFAULT_COMMUNITY_FEEDS =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
ClassifiedsEvent.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(channel: PublicChatChannel) = runIOCatching { account.follow(channel) }
|
||||
|
||||
+123
-41
@@ -20,29 +20,56 @@
|
||||
*/
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
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.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.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
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.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.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.loggedIn.AccountViewModel
|
||||
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.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 kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
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
|
||||
fun CommunityScreen(
|
||||
note: AddressableNote,
|
||||
feedViewModel: CommunityFeedViewModel,
|
||||
modFeedViewModel: CommunityModerationFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchLifecycleAndUpdateModel(feedViewModel)
|
||||
WatchLifecycleAndUpdateModel(modFeedViewModel)
|
||||
CommunityFilterAssemblerSubscription(note, accountViewModel.dataSources().community)
|
||||
|
||||
val pagerState = rememberForeverPagerState(note.idHex + "CommunityScreenPagerState") { 2 }
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
isActive = { !expanded.value },
|
||||
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 = {
|
||||
NewCommunityNoteButton(note.idHex, accountViewModel, nav)
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(it).consumeWindowInsets(it),
|
||||
) {
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
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)
|
||||
HorizontalPager(
|
||||
contentPadding = it,
|
||||
state = pagerState,
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 ->
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
1 ->
|
||||
RefresheableFeedView(
|
||||
modFeedViewModel,
|
||||
null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -57,8 +57,8 @@ fun NewCommunityNoteButton(
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
val route =
|
||||
Route.NewShortNote(
|
||||
baseReplyTo = note.idHex,
|
||||
Route.GenericCommentPost(
|
||||
replyTo = note.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
},
|
||||
|
||||
+32
-21
@@ -26,21 +26,28 @@ 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.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
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
|
||||
import kotlin.collections.toSet
|
||||
|
||||
class CommunityFeedFilter(
|
||||
val note: AddressableNote,
|
||||
val communityDefNote: AddressableNote,
|
||||
val account: Account,
|
||||
) : 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> {
|
||||
val myPubKey = account.userProfile().pubkeyHex
|
||||
if (communityDefEvent == null) return emptyList()
|
||||
|
||||
val result =
|
||||
LocalCache.notes.mapFlattenIntoSet { _, it ->
|
||||
filterMap(it, myPubKey)
|
||||
filterMap(it)
|
||||
}
|
||||
|
||||
return sort(result)
|
||||
@@ -49,35 +56,39 @@ class CommunityFeedFilter(
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val myPubKey = account.userProfile().pubkeyHex
|
||||
if (communityDefEvent == null) return emptySet()
|
||||
|
||||
return collection
|
||||
.mapNotNull {
|
||||
filterMap(it, myPubKey)
|
||||
filterMap(it)
|
||||
}.flatten()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
private fun filterMap(
|
||||
note: Note,
|
||||
myPubKey: HexKey,
|
||||
): List<Note>? =
|
||||
if (
|
||||
private fun filterMap(note: Note): List<Note>? {
|
||||
val noteEvent = note.event ?: return null
|
||||
|
||||
return if (
|
||||
// Only Approvals
|
||||
note.event is CommunityPostApprovalEvent &&
|
||||
// Of the given community
|
||||
note.event?.isTaggedAddressableNote(this.note.idHex) == true
|
||||
(noteEvent is TextNoteEvent || noteEvent is CommentEvent || noteEvent is CommunityPostApprovalEvent) &&
|
||||
noteEvent.pubKey in moderators &&
|
||||
noteEvent.isForCommunity(this.communityDefNote.idHex)
|
||||
) {
|
||||
// if it is my post, bring on
|
||||
if (note.author?.pubkeyHex == myPubKey && note.isNewThread()) {
|
||||
listOf(note)
|
||||
if (noteEvent is CommunityPostApprovalEvent) {
|
||||
note.replyTo?.filter {
|
||||
it.isNewThread()
|
||||
}
|
||||
} else {
|
||||
// brings the actual posts, not the approvals
|
||||
note.replyTo?.filter { it.isNewThread() }
|
||||
if (note.isNewThread()) {
|
||||
listOf(note)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+28
-7
@@ -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.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||
|
||||
class CommunityFeedFilterSubAssembler(
|
||||
client: NostrClient,
|
||||
@@ -40,14 +41,34 @@ class CommunityFeedFilterSubAssembler(
|
||||
return keys.flatMap {
|
||||
val commEvent = it.community.event
|
||||
if (commEvent is CommunityDefinitionEvent) {
|
||||
val relays =
|
||||
commEvent.relayUrls().ifEmpty { null }
|
||||
?: LocalCache.relayHints.hintsForAddress(commEvent.addressTag()).ifEmpty { null }
|
||||
?: it.community.relayUrls()
|
||||
val communityRelays =
|
||||
(
|
||||
commEvent.relayUrls().ifEmpty {
|
||||
LocalCache.relayHints.hintsForAddress(commEvent.addressTag())
|
||||
} + it.community.relayUrls()
|
||||
).toSet()
|
||||
|
||||
relays.toSet().map {
|
||||
filterCommunityPosts(it, commEvent, since?.get(it)?.time)
|
||||
}
|
||||
val moderatorsOutbox =
|
||||
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 {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
+54
-2
@@ -20,11 +20,29 @@
|
||||
*/
|
||||
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.filters.Filter
|
||||
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.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(
|
||||
relay: NormalizedRelayUrl,
|
||||
@@ -35,10 +53,44 @@ fun filterCommunityPosts(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
authors = community.moderators().map { it.pubKey }.plus(community.pubKey),
|
||||
authors = community.moderatorKeys(),
|
||||
tags = mapOf("a" to listOf(community.addressTag())),
|
||||
kinds = CommunityPostApprovalEvent.KIND_LIST,
|
||||
kinds = CommunityPostKinds,
|
||||
limit = 500,
|
||||
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.tags.people.isTaggedUser
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
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.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
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.BadgeProfilesEvent
|
||||
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.nip90Dvms.NIP90ContentDiscoveryRequestEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||
@@ -157,7 +161,26 @@ class NotificationFeedFilter(
|
||||
}
|
||||
|
||||
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 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.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
@@ -70,6 +71,7 @@ class UserProfileNewThreadFeedFilter(
|
||||
it.author == user &&
|
||||
(
|
||||
it.event is TextNoteEvent ||
|
||||
it.event is CommentEvent ||
|
||||
it.event is ClassifiedsEvent ||
|
||||
it.event is RepostEvent ||
|
||||
it.event is GenericRepostEvent ||
|
||||
|
||||
+59
-16
@@ -49,6 +49,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.model.LocalCache
|
||||
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.GenericLoadable
|
||||
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.NoteCompose
|
||||
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.RenderApproveButton
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderDraft
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderRepost
|
||||
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.EditFieldTrailingIconModifier
|
||||
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.Size5dp
|
||||
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.zapPolls.PollNoteEvent
|
||||
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.nip04Dm.messages.PrivateDmEvent
|
||||
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.nip71Video.VideoEvent
|
||||
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.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||
@@ -435,6 +440,7 @@ private fun FullBleedNoteCompose(
|
||||
) {
|
||||
val noteEvent = baseNote.event ?: return
|
||||
|
||||
val isDraft = baseNote.isDraft()
|
||||
val backgroundColor =
|
||||
calculateBackgroundColor(
|
||||
baseNote.createdAt(),
|
||||
@@ -467,15 +473,28 @@ private fun FullBleedNoteCompose(
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||
|
||||
val isCommunityPost =
|
||||
remember(baseNote) {
|
||||
baseNote.event?.isTaggedAddressableKind(CommunityDefinitionEvent.KIND) == true
|
||||
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) {
|
||||
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||
if (isCommunityPost) {
|
||||
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
CheckAndDisplayEditStatus(editState)
|
||||
@@ -508,7 +527,7 @@ private fun FullBleedNoteCompose(
|
||||
DisplayPoW(pow)
|
||||
}
|
||||
|
||||
if (baseNote.isDraft()) {
|
||||
if (isDraft) {
|
||||
DisplayDraft()
|
||||
}
|
||||
|
||||
@@ -527,9 +546,7 @@ private fun FullBleedNoteCompose(
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
modifier
|
||||
.padding(horizontal = 12.dp),
|
||||
modifier = PaddingHorizontal12Modifier,
|
||||
) {
|
||||
Column {
|
||||
if (noteEvent is ChannelCreateEvent) {
|
||||
@@ -753,17 +770,43 @@ private fun FullBleedNoteCompose(
|
||||
val zapSplits = remember(noteEvent) { noteEvent.hasZapSplitSetup() }
|
||||
if (zapSplits) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
) {
|
||||
Row(PaddingHorizontal12Modifier) {
|
||||
DisplayZapSplits(noteEvent, false, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
RenderApprovalIfNeeded(baseNote, accountViewModel, 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
|
||||
private fun RenderClassifiedsReaderForThread(
|
||||
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 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="new_threads">New Threads</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="replies">Replies</string>
|
||||
<string name="mutual">Yours</string>
|
||||
@@ -806,6 +808,10 @@
|
||||
<string name="created_at">Created at</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="status_update">Update your status</string>
|
||||
|
||||
+46
-44
@@ -81,50 +81,13 @@ fun TranslatableRichTextViewer(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var translatedTextState by translateAndWatchLanguageChanges(content, id, accountViewModel)
|
||||
|
||||
CrossfadeIfEnabled(targetState = translatedTextState, accountViewModel = accountViewModel) {
|
||||
RenderText(
|
||||
translatedTextState = it,
|
||||
content = content,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = id,
|
||||
callbackUri = callbackUri,
|
||||
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 {
|
||||
TranslatableRichTextViewer(
|
||||
content = content,
|
||||
id = id,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
ExpandableRichTextViewer(
|
||||
toBeViewed,
|
||||
it,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
modifier,
|
||||
@@ -135,6 +98,45 @@ private fun RenderText(
|
||||
accountViewModel,
|
||||
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 (
|
||||
translatedTextState.sourceLang != null &&
|
||||
@@ -325,7 +327,7 @@ fun translateAndWatchLanguageChanges(
|
||||
id: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
): MutableState<TranslationConfig> {
|
||||
var translatedTextState = remember(id) { mutableStateOf(TranslationsCache.get(content)) }
|
||||
val translatedTextState = remember(id) { mutableStateOf(TranslationsCache.get(content)) }
|
||||
|
||||
TranslateAndWatchLanguageChanges(
|
||||
content,
|
||||
|
||||
@@ -32,8 +32,12 @@ fun Event.isTaggedAddressableNotes(addressIds: Set<String>) = tags.isTaggedAddre
|
||||
|
||||
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(kindStr: String) = tags.getTagOfAddressableKind(kindStr)
|
||||
|
||||
fun Event.taggedATags() = tags.taggedATags()
|
||||
|
||||
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(kindStr: String) = this.any(ATag::isTaggedWithKind, kindStr)
|
||||
|
||||
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.firstTaggedATag() = this.fastFirstNotNullOfOrNull(ATag::parse)
|
||||
|
||||
@@ -72,16 +72,18 @@ open class BaseThreadedEvent(
|
||||
return newStyleReply ?: newStyleRoot ?: oldStylePositional
|
||||
}
|
||||
|
||||
fun tagsWithoutCitations(): List<String> {
|
||||
open fun tagsWithoutCitations(): List<String> {
|
||||
val certainRepliesTo = markedReplyTos()
|
||||
val uncertainRepliesTo = unmarkedReplyTos()
|
||||
|
||||
val tagAddresses =
|
||||
taggedATags()
|
||||
.filter {
|
||||
it.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || it.kind != WikiNoteEvent.KIND)
|
||||
.filter { aTag ->
|
||||
aTag.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || aTag.kind != WikiNoteEvent.KIND)
|
||||
// removes forks from itself.
|
||||
}.map { it.toTag() }
|
||||
}.map {
|
||||
it.toTag()
|
||||
}
|
||||
|
||||
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.HexKey
|
||||
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.EventHintBundle
|
||||
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.PubKeyHint
|
||||
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.nip10Notes.BaseThreadedEvent
|
||||
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.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull
|
||||
import kotlin.collections.mapNotNull
|
||||
|
||||
@Immutable
|
||||
class CommentEvent(
|
||||
@@ -126,6 +130,28 @@ class CommentEvent(
|
||||
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 replyAuthor() = tags.firstNotNullOfOrNull(ReplyAuthorTag::parse)
|
||||
@@ -142,14 +168,6 @@ class CommentEvent(
|
||||
|
||||
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 */
|
||||
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 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) }
|
||||
|
||||
@@ -184,12 +202,31 @@ class CommentEvent(
|
||||
tags.lastNotNullOfOrNull(ReplyEventTag::parseKey)
|
||||
?: tags.lastNotNullOfOrNull(RootEventTag::parseKey)
|
||||
|
||||
fun rootAddress() = tags.mapNotNull(RootAddressTag::parseAddress)
|
||||
|
||||
fun rootAddressId() = tags.mapNotNull(RootAddressTag::parseAddressId)
|
||||
|
||||
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 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 {
|
||||
const val KIND = 1111
|
||||
const val ALT = "Reply to "
|
||||
|
||||
@@ -44,6 +44,12 @@ class ReplyAddressTag(
|
||||
@JvmStatic
|
||||
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
|
||||
fun parse(tag: Array<String>): ReplyAddressTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
@@ -55,6 +61,15 @@ class ReplyAddressTag(
|
||||
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
|
||||
fun parseAddressId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
|
||||
@@ -67,6 +67,15 @@ class RootAddressTag(
|
||||
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
|
||||
fun parseValidAddress(tag: Array<String>): String? {
|
||||
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.HexKey
|
||||
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.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags
|
||||
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.addressables.ATag.Companion.isTagged
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
|
||||
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.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.follow.tags.CommunityTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.collections.mapNotNull
|
||||
|
||||
@Immutable
|
||||
class CommunityPostApprovalEvent(
|
||||
@@ -54,13 +61,17 @@ class CommunityPostApprovalEvent(
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
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)
|
||||
|
||||
@@ -77,15 +88,17 @@ class CommunityPostApprovalEvent(
|
||||
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 {
|
||||
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 moderatorKeys() = tags.mapNotNull(ModeratorTag::parseKey)
|
||||
fun moderatorKeys() = tags.mapNotNull(ModeratorTag::parseKey).plus(pubKey)
|
||||
|
||||
fun relays() = tags.mapNotNull(RelayTag::parse)
|
||||
|
||||
@@ -88,6 +88,7 @@ class CommunityDefinitionEvent(
|
||||
|
||||
companion object {
|
||||
const val KIND = 34550
|
||||
const val KIND_STR = "34550"
|
||||
const val ALT_DESCRIPTION = "Community definition"
|
||||
|
||||
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.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
|
||||
@@ -47,7 +48,7 @@ class CommunityTag(
|
||||
const val TAG_NAME = "a"
|
||||
|
||||
@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
|
||||
fun isTagged(
|
||||
@@ -61,12 +62,6 @@ class CommunityTag(
|
||||
address: CommunityTag,
|
||||
) = 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 isTaggedWithKind(
|
||||
tag: Array<String>,
|
||||
@@ -74,26 +69,27 @@ class CommunityTag(
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], kind)
|
||||
|
||||
@JvmStatic
|
||||
fun parse(
|
||||
aTagId: String,
|
||||
relay: String?,
|
||||
) = Address.parse(aTagId)?.let {
|
||||
CommunityTag(it, relay?.let { RelayUrlNormalizer.normalizeOrNull(it) })
|
||||
}
|
||||
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>): CommunityTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return parse(tag[1], tag.getOrNull(2))
|
||||
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 CommunityTag(address, relayHint)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseValidAddress(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { 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()
|
||||
}
|
||||
|
||||
@@ -102,14 +98,16 @@ class CommunityTag(
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { 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
|
||||
fun parseAddressId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { 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]
|
||||
}
|
||||
|
||||
@@ -117,8 +115,7 @@ class CommunityTag(
|
||||
fun parseAsHint(tag: Array<String>): AddressHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[1].contains(':')) { return null }
|
||||
ensure(Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
|
||||
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||
|
||||
Reference in New Issue
Block a user