mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
Merge branch 'main' of https://github.com/vitorpamplona/amethyst ite resolve branch.
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
/.idea/other.xml
|
||||
/.idea/runConfigurations.xml
|
||||
/.idea/ChatHistory_schema_v2.xml
|
||||
/.idea/artifacts/*
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div align="center">
|
||||
<div align="center">
|
||||
|
||||
<a href="https://amethyst.social">
|
||||
<img src="./docs/design/3rd%20Logo%20-%20Zitron/amethyst.svg" alt="Amethyst Logo" title="Amethyst logo" width="80"/>
|
||||
|
||||
@@ -44,8 +44,8 @@ android {
|
||||
applicationId = "com.vitorpamplona.amethyst"
|
||||
minSdk = libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode = 424
|
||||
versionName = generateVersionName("1.00.5")
|
||||
versionCode = 425
|
||||
versionName = generateVersionName("1.01.0")
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.service.ots.OkHttpBitcoinExplorer
|
||||
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
|
||||
import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpBitcoinExplorer
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpCalendarBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
+10
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -52,3 +54,11 @@ fun TranslatableRichTextViewer(
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun TranslatableRichTextViewer(
|
||||
content: String,
|
||||
id: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
displayText: @Composable (String) -> Unit,
|
||||
) = displayText(content)
|
||||
|
||||
@@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
|
||||
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
|
||||
import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor
|
||||
import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
|
||||
@@ -51,6 +50,7 @@ import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@@ -89,7 +89,6 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
|
||||
import com.vitorpamplona.amethyst.model.torState.TorRelayState
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder
|
||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet
|
||||
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
|
||||
@@ -137,6 +136,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
@@ -187,6 +187,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
|
||||
@@ -323,7 +325,9 @@ class Account(
|
||||
|
||||
val otsResolverBuilder: OkHttpOtsResolverBuilder =
|
||||
OkHttpOtsResolverBuilder(
|
||||
Amethyst.instance.okHttpClients,
|
||||
{
|
||||
Amethyst.instance.okHttpClients.getHttpClient(privacyState.shouldUseTorForMoneyOperations(it))
|
||||
},
|
||||
privacyState::shouldUseTorForMoneyOperations,
|
||||
Amethyst.instance.otsBlockHeightCache,
|
||||
)
|
||||
@@ -879,6 +883,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?) {
|
||||
|
||||
@@ -34,7 +34,6 @@ object Constants {
|
||||
val elites = RelayUrlNormalizer.normalize("wss://nostrelites.org")
|
||||
|
||||
val bitcoiner = RelayUrlNormalizer.normalize("wss://nostr.bitcoiner.social")
|
||||
val bg = RelayUrlNormalizer.normalize("wss://relay.nostr.bg")
|
||||
val oxtr = RelayUrlNormalizer.normalize("wss://nostr.oxtr.dev")
|
||||
val fmtwiz = RelayUrlNormalizer.normalize("wss://nostr.fmt.wiz.biz")
|
||||
|
||||
@@ -47,7 +46,7 @@ object Constants {
|
||||
val coracle = RelayUrlNormalizer.normalize("wss://indexer.coracle.social")
|
||||
val userkinds = RelayUrlNormalizer.normalize("wss://user.kindpag.es")
|
||||
|
||||
val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr, fmtwiz, bg)
|
||||
val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr, fmtwiz)
|
||||
|
||||
val defaultSearchRelaySet = setOf(band, wine, where)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,10 +24,10 @@ import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.util.Base64
|
||||
|
||||
+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(
|
||||
|
||||
@@ -40,11 +40,6 @@ abstract class AdditiveFeedFilter<T> :
|
||||
oldList
|
||||
}
|
||||
}
|
||||
|
||||
fun removeNotes(
|
||||
oldList: List<T>,
|
||||
deletedNotes: Set<T>,
|
||||
): List<T> = oldList - deletedNotes
|
||||
}
|
||||
|
||||
interface IAdditiveFeedFilter<T> : IFeedFilter<T> {
|
||||
|
||||
@@ -105,6 +105,8 @@ class ChannelFeedContentState(
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFromFeed(deletedNotes: Set<Note>) {}
|
||||
|
||||
fun refreshFromOldState(newItems: Set<Note>) {
|
||||
val oldNotesState = _feedContent.value
|
||||
if (oldNotesState is ChannelFeedState.Loaded) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
+28
-6
@@ -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()
|
||||
@@ -317,7 +323,7 @@ open class CommentPostViewModel :
|
||||
cancel()
|
||||
|
||||
accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast)
|
||||
accountViewModel.viewModelScope.launch {
|
||||
accountViewModel.viewModelScope.launch(Dispatchers.Default) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(version)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
+9
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.nip22Comments
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -135,6 +136,14 @@ fun GenericCommentPostScreen(
|
||||
) {
|
||||
WatchAndLoadMyEmojiList(accountViewModel)
|
||||
|
||||
BackHandler {
|
||||
accountViewModel.runIOCatching {
|
||||
postViewModel.sendDraftSync()
|
||||
postViewModel.cancel()
|
||||
}
|
||||
nav.popBack()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
PostingTopBar(
|
||||
|
||||
+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,
|
||||
|
||||
+28
@@ -104,6 +104,34 @@ class AccountFeedContentStates(
|
||||
drafts.updateFeedWith(newNotes)
|
||||
}
|
||||
|
||||
fun deleteNotes(newNotes: Set<Note>) {
|
||||
checkNotInMainThread()
|
||||
|
||||
homeLive.deleteFromFeed(newNotes)
|
||||
homeNewThreads.deleteFromFeed(newNotes)
|
||||
homeReplies.deleteFromFeed(newNotes)
|
||||
|
||||
dmKnown.updateFeedWith(newNotes)
|
||||
dmNew.updateFeedWith(newNotes)
|
||||
|
||||
videoFeed.deleteFromFeed(newNotes)
|
||||
|
||||
discoverMarketplace.deleteFromFeed(newNotes)
|
||||
discoverFollowSets.deleteFromFeed(newNotes)
|
||||
discoverReads.deleteFromFeed(newNotes)
|
||||
discoverDVMs.deleteFromFeed(newNotes)
|
||||
discoverLive.deleteFromFeed(newNotes)
|
||||
discoverCommunities.deleteFromFeed(newNotes)
|
||||
discoverPublicChats.deleteFromFeed(newNotes)
|
||||
|
||||
notifications.deleteFromFeed(newNotes)
|
||||
notificationSummary.invalidateInsertData(newNotes)
|
||||
|
||||
feedListOptions.deleteFromFeed(newNotes)
|
||||
|
||||
drafts.deleteFromFeed(newNotes)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
notifications.destroy()
|
||||
notificationSummary.destroy()
|
||||
|
||||
+6
@@ -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) }
|
||||
@@ -1188,6 +1193,7 @@ class AccountViewModel(
|
||||
}
|
||||
logTime("AccountViewModel deletedEventBundle Update with ${newNotes.size} new notes") {
|
||||
newNotesPreProcessor.runDeleted(newNotes)
|
||||
feedStates.deleteNotes(newNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -351,7 +351,7 @@ class ChatNewMessageViewModel :
|
||||
val version = draftTag.current
|
||||
innerSendPost(null)
|
||||
cancel()
|
||||
accountViewModel.viewModelScope.launch {
|
||||
accountViewModel.viewModelScope.launch(Dispatchers.Default) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(version)
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -165,6 +166,14 @@ fun NewGroupDMScreen(
|
||||
|
||||
WatchAndLoadMyEmojiList(accountViewModel)
|
||||
|
||||
BackHandler {
|
||||
accountViewModel.runIOCatching {
|
||||
postViewModel.sendDraftSync()
|
||||
postViewModel.cancel()
|
||||
}
|
||||
nav.popBack()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
PostingTopBar(
|
||||
|
||||
+9
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -79,6 +80,14 @@ fun PrivateMessageEditFieldRow(
|
||||
onSendNewMessage: () -> Unit,
|
||||
nav: INav,
|
||||
) {
|
||||
BackHandler {
|
||||
accountViewModel.runIOCatching {
|
||||
channelScreenModel.sendDraftSync()
|
||||
channelScreenModel.cancel()
|
||||
}
|
||||
nav.popBack()
|
||||
}
|
||||
|
||||
channelScreenModel.replyTo.value?.let {
|
||||
DisplayReplyingToNote(it, accountViewModel, nav) {
|
||||
channelScreenModel.clearReply()
|
||||
|
||||
+1
-1
@@ -276,7 +276,7 @@ open class ChannelNewMessageViewModel :
|
||||
cancel()
|
||||
|
||||
accountViewModel.account.signAndSendPrivately(template, channelRelays)
|
||||
accountViewModel.viewModelScope.launch {
|
||||
accountViewModel.viewModelScope.launch(Dispatchers.Default) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(version)
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -60,6 +61,14 @@ fun EditFieldRow(
|
||||
onSendNewMessage: suspend () -> Unit,
|
||||
nav: INav,
|
||||
) {
|
||||
BackHandler {
|
||||
accountViewModel.runIOCatching {
|
||||
channelScreenModel.sendDraftSync()
|
||||
channelScreenModel.cancel()
|
||||
}
|
||||
nav.popBack()
|
||||
}
|
||||
|
||||
channelScreenModel.replyTo.value?.let {
|
||||
DisplayReplyingToNote(it, accountViewModel, nav) {
|
||||
channelScreenModel.clearReply()
|
||||
|
||||
+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,
|
||||
),
|
||||
)
|
||||
|
||||
+9
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -136,6 +137,14 @@ fun NewProductScreen(
|
||||
) {
|
||||
WatchAndLoadMyEmojiList(accountViewModel)
|
||||
|
||||
BackHandler {
|
||||
accountViewModel.runIOCatching {
|
||||
postViewModel.sendDraftSync()
|
||||
postViewModel.cancel()
|
||||
}
|
||||
nav.popBack()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
PostingTopBar(
|
||||
|
||||
+1
-1
@@ -297,7 +297,7 @@ open class NewProductViewModel :
|
||||
cancel()
|
||||
|
||||
accountViewModel.account.signAndSendPrivatelyOrBroadcast(template, relayList = { relayList })
|
||||
accountViewModel.viewModelScope.launch {
|
||||
accountViewModel.viewModelScope.launch(Dispatchers.Default) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(version)
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -164,6 +165,14 @@ private fun NewPostScreenInner(
|
||||
) {
|
||||
WatchAndLoadMyEmojiList(accountViewModel)
|
||||
|
||||
BackHandler {
|
||||
accountViewModel.runIOCatching {
|
||||
postViewModel.sendDraftSync()
|
||||
postViewModel.cancel()
|
||||
}
|
||||
nav.popBack()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
PostingTopBar(
|
||||
|
||||
+1
-1
@@ -483,7 +483,7 @@ open class ShortNotePostViewModel :
|
||||
cancel()
|
||||
|
||||
accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast)
|
||||
accountViewModel.viewModelScope.launch {
|
||||
accountViewModel.viewModelScope.launch(Dispatchers.Default) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(version)
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -340,6 +340,9 @@ class CardFeedContentState(
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFromFeed(deletedNotes: Set<Note>) {
|
||||
}
|
||||
|
||||
private fun refreshFromOldState(newItems: Set<Note>) {
|
||||
val oldNotesState = _feedContent.value
|
||||
|
||||
|
||||
+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)
|
||||
|
||||
+9
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -128,6 +129,14 @@ fun NewPublicMessageScreen(
|
||||
|
||||
WatchAndLoadMyEmojiList(accountViewModel)
|
||||
|
||||
BackHandler {
|
||||
accountViewModel.runIOCatching {
|
||||
postViewModel.sendDraftSync()
|
||||
postViewModel.cancel()
|
||||
}
|
||||
nav.popBack()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
PostingTopBar(
|
||||
|
||||
+1
-1
@@ -321,7 +321,7 @@ class NewPublicMessageViewModel :
|
||||
cancel()
|
||||
|
||||
accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast)
|
||||
accountViewModel.viewModelScope.launch {
|
||||
accountViewModel.viewModelScope.launch(Dispatchers.Default) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(version)
|
||||
}
|
||||
}
|
||||
|
||||
+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,
|
||||
|
||||
@@ -358,3 +358,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)
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<string name="error_parsing_preview_for">"Błąd analizowania podglądu dla %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"Podgląd obrazu karty dla %1$s"</string>
|
||||
<string name="new_channel">Nowy kanał</string>
|
||||
<string name="channel_name">Nazwa Grupy</string>
|
||||
<string name="channel_name">Nazwa</string>
|
||||
<string name="my_awesome_group">Nazwa Grupy</string>
|
||||
<string name="picture_url">Adres Url obrazka</string>
|
||||
<string name="description">Opis</string>
|
||||
@@ -159,10 +159,11 @@
|
||||
<string name="edits_the_channel_metadata">Edytuje metadane kanału</string>
|
||||
<string name="join">Dołącz</string>
|
||||
<string name="known">Znajomi</string>
|
||||
<string name="new_requests">Nowe Kontakty</string>
|
||||
<string name="new_requests">Nowe</string>
|
||||
<string name="blocked_users">Zablokowani użytkownicy</string>
|
||||
<string name="new_threads">Nowe Wpisy</string>
|
||||
<string name="conversations">Komentarze</string>
|
||||
<string name="mod_queue">Moderacja</string>
|
||||
<string name="notes">Wpisy</string>
|
||||
<string name="replies">Odpowiedzi</string>
|
||||
<string name="mutual">Twoje</string>
|
||||
@@ -263,9 +264,9 @@
|
||||
<string name="report_hateful_speech">Zgłoś mowę nienawiści</string>
|
||||
<string name="report_nudity_porn">Zgłoś Nagość / Pornografię</string>
|
||||
<string name="others">inne</string>
|
||||
<string name="mark_all_known_as_read">Oznacz wszystkie popularne jako przeczytane</string>
|
||||
<string name="mark_all_new_as_read">Oznacz wszystkie nowe jako przeczytane</string>
|
||||
<string name="mark_all_as_read">Oznacz wszystkie jako przeczytane</string>
|
||||
<string name="mark_all_known_as_read">Zaznacz wszystkie popularne jako przeczytane</string>
|
||||
<string name="mark_all_new_as_read">Zaznacz wszystkie nowe jako przeczytane</string>
|
||||
<string name="mark_all_as_read">Zaznacz wszystkie jako przeczytane</string>
|
||||
<string name="backup_keys">Klucze kopii zapasowej</string>
|
||||
<string name="account_backup_tips2_md" tools:ignore="Typos"> ## Porady dotyczące kopii zapasowej i bezpieczeństwa
|
||||
\n\nTwoje konto jest zabezpieczone tajnym kluczem. Kluczem jest długa sekwencja znaków, zaczynająca się od **nsec1**. Każdy, kto ma dostęp do tego tajnego klucza może publikować i zmieniać Twoją tożsamość.
|
||||
@@ -456,7 +457,7 @@
|
||||
<string name="tor_use_onion_address_explainer">Użyj Tor dla dowolnego adresu .onion</string>
|
||||
<string name="tor_use_dm_relays">Transmitery DM</string>
|
||||
<string name="tor_use_dm_relays_explainer">Zmuś Tor do wysyłania i odbierania DM</string>
|
||||
<string name="tor_use_new_relays">Niepewne Transmitery</string>
|
||||
<string name="tor_use_new_relays">Niewiarygodne Transmitery</string>
|
||||
<string name="tor_use_new_relays_explainer">Wymuś Tor na transmiterach skrzynki odbiorczej/nadawczej</string>
|
||||
<string name="tor_use_trusted_relays">Zaufane Transmitery</string>
|
||||
<string name="tor_use_trusted_relays_explainer">Wymuś Tor na wszystkich transmiterach na listach</string>
|
||||
@@ -471,10 +472,10 @@
|
||||
<string name="tor_use_money_operations">Operacje finansowe</string>
|
||||
<string name="tor_use_money_operations_explainer">Wymuś Tor przy zapsach, transferach lightning and cashu</string>
|
||||
<string name="tor_use_nip05_verification">Weryfikacja adresu Nostr</string>
|
||||
<string name="tor_use_nip05_verification_explainer">Wymuś Tor przy weryfikacji adresów NIP-05</string>
|
||||
<string name="tor_use_nip05_verification_explainer">Wymuś Tor podczas weryfikacji adresów NIP-05</string>
|
||||
<string name="tor_use_nip96_uploads">Wgrywanie mediów</string>
|
||||
<string name="tor_use_nip96_uploads_explainer">Wymuś Tor podczas wgrywania zawartości</string>
|
||||
<string name="tor_internal">Wewnętrzny</string>
|
||||
<string name="tor_internal">Wewnętrznie</string>
|
||||
<string name="tor_external">Orbot</string>
|
||||
<string name="tor_off">Wył.</string>
|
||||
<string name="tor_when_needed">Podstawowe</string>
|
||||
@@ -484,7 +485,7 @@
|
||||
<string name="tor_custom">Niestandardowy</string>
|
||||
<string name="tor_when_needed_explainer">Użyj Tor, gdy jest wymagany przez serwer</string>
|
||||
<string name="tor_default_explainer">Ukryj swój adres IP przed przypadkowymi transmiterami</string>
|
||||
<string name="tor_small_payloads_explainer">Ukryj swój adres IP od wszystkiego oprócz zdjęć i filmów</string>
|
||||
<string name="tor_small_payloads_explainer">Ukryj swój adres IP wszędzie oprócz zdjęć i filmów</string>
|
||||
<string name="tor_full_privacy_explainer">Ukryj swój adres IP przy wszystkich połączeniach</string>
|
||||
<string name="tor_custom_explainer">Stwórz swój własny</string>
|
||||
<string name="invalid_port_number">Nieprawidłowy numer portu</string>
|
||||
@@ -520,7 +521,7 @@
|
||||
<string name="warn_when_posts_have_reports_from_your_follows_title">Ostrzeżenie o zgłoszeniach</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows_explainer">Wyświetla komunikat ostrzegawczy, gdy posty zostały zgłoszone przez 5 lub więcej obserwowanych osób</string>
|
||||
<string name="show_sensitive_content_title">Pokaż wrażliwe treści</string>
|
||||
<string name="show_sensitive_content_explainer">Pokazuje komunikat ostrzegawczy, gdy autor wpisu oznaczył go jako wrażliwy</string>
|
||||
<string name="show_sensitive_content_explainer">Pokazuje komunikat ostrzegawczy, gdy autor oznaczył wpis jako wrażliwy</string>
|
||||
<string name="new_reaction_symbol">Nowy Symbol Odzewu</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">Brak wstępnie wybranych typów reakcji dla tego użytkownika. Przytrzymaj przycisk serce, aby zmienić</string>
|
||||
<string name="zapraiser">Zapraiser</string>
|
||||
@@ -645,7 +646,7 @@
|
||||
<string name="messages_new_message_to">Do</string>
|
||||
<string name="messages_new_message_subject">Temat</string>
|
||||
<string name="messages_new_message_subject_caption">Temat dyskusji</string>
|
||||
<string name="messages_new_message_to_caption">"\@Użytkownik1, @Użytkownik2, @Użytkownik3"</string>
|
||||
<string name="messages_new_message_to_caption">"\@Użytkownik1, @Użytkownik2"</string>
|
||||
<string name="messages_cant_upload_title">Nie można wysłać</string>
|
||||
<string name="messages_cant_upload_explainer">Najpierw podaj adresata wiadomości</string>
|
||||
<string name="messages_group_descriptor">Członkowie tej grupy</string>
|
||||
@@ -669,6 +670,9 @@
|
||||
<string name="media_added_to_profile_gallery">Pliki dodane do Twojej galerii profilu</string>
|
||||
<string name="created_at">Utworzono</string>
|
||||
<string name="rules">Zasady</string>
|
||||
<string name="community_description">O nas</string>
|
||||
<string name="guidelines">Wskazówki</string>
|
||||
<string name="moderators">Moderatorzy</string>
|
||||
<string name="login_with_external_signer">Zaloguj się za pomocą Amber</string>
|
||||
<string name="status_update">Zaktualizuj swój status</string>
|
||||
<string name="lightning_wallets_not_found">Błąd podczas analizowania komunikatu o błędzie</string>
|
||||
@@ -903,7 +907,7 @@
|
||||
<string name="trusted_section_explainer">Transmitery, którym ufasz, nie potrzebujesz połączenia Tor</string>
|
||||
<string name="proxy_relays_title">Transmitery Proxy</string>
|
||||
<string name="proxy_section">Transmitery Proxy</string>
|
||||
<string name="proxy_section_explainer">Transmitery agregatora, z których aplikacja musi pobierać kanały, takie jak filter.nostr.wine. To zastępuje model skrzynki nadawczej i sprawia, że aplikacja łączy się tylko z transmiterami z listy.</string>
|
||||
<string name="proxy_section_explainer">Transmitery agregatora, z których aplikacja musi pobierać kanały, takie jak filter.nostr.wine. To zastępuje model skrzynki nadawczej i sprawia, że aplikacja łączy się tylko z transmiterami z Twojej listy.</string>
|
||||
<string name="broadcast_relays_title">Transmitery Nadawcze</string>
|
||||
<string name="broadcast_section">Transmitery Nadawania</string>
|
||||
<string name="broadcast_section_explainer">Transmitery, które specjalizują się w przesyłaniu wpisów do wszystkich innych transmiterów, takich jak sendit.nosflare.com. Amethyst doda ten transmiter do wszystkich nowych wydarzeń utworzonych przez użytkownika</string>
|
||||
@@ -1011,4 +1015,6 @@
|
||||
<string name="dont_translate_from_description">Języki wyświetlane tutaj nie będą tłumaczone. Wybierz język, aby usunąć go z listy języków nietłumaczonych.</string>
|
||||
<string name="pause">Pauza</string>
|
||||
<string name="play">Odtwórz</string>
|
||||
<string name="open_dropdown_menu">Otwórz rozwijane menu</string>
|
||||
<string name="option_of">Opcja %1$s dla %2$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -163,6 +163,8 @@
|
||||
<string name="blocked_users">已隐藏用户</string>
|
||||
<string name="new_threads">新动态</string>
|
||||
<string name="conversations">会话</string>
|
||||
<string name="feed">源</string>
|
||||
<string name="mod_queue">Mod 队列</string>
|
||||
<string name="notes">动态</string>
|
||||
<string name="replies">回复</string>
|
||||
<string name="mutual">你的</string>
|
||||
@@ -672,6 +674,9 @@
|
||||
<string name="media_added_to_profile_gallery">媒体已添加到个人资料相册中</string>
|
||||
<string name="created_at">创建于</string>
|
||||
<string name="rules">规则</string>
|
||||
<string name="community_description">关于我们</string>
|
||||
<string name="guidelines">指南</string>
|
||||
<string name="moderators">审核者</string>
|
||||
<string name="login_with_external_signer">使用 Amber 登录</string>
|
||||
<string name="status_update">更新你的状态</string>
|
||||
<string name="lightning_wallets_not_found">错误解析错误消息</string>
|
||||
@@ -1014,4 +1019,8 @@
|
||||
<string name="dont_translate_from_description">此处显示的语言不会被翻译,请选择一种目标语言重新翻译并去除这个提示。</string>
|
||||
<string name="pause">暂停</string>
|
||||
<string name="play">播放</string>
|
||||
<string name="open_dropdown_menu">打开下拉菜单</string>
|
||||
<string name="option_of">第 %1$s 个选项,共 %2$s 个</string>
|
||||
<string name="feed_filter_selected">源过滤器,选中了 %1$s</string>
|
||||
<string name="feed_filter_select_an_option">源过滤器,%1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -54,10 +54,10 @@ dependencies {
|
||||
implementation libs.androidx.core.ktx
|
||||
|
||||
// Compose's @Immutable and @Stable classes
|
||||
implementation libs.androidx.runtime.runtime
|
||||
|
||||
implementation libs.androidx.compose.runtime
|
||||
implementation project(path: ':quartz')
|
||||
|
||||
|
||||
testImplementation libs.junit
|
||||
|
||||
androidTestImplementation libs.androidx.junit
|
||||
|
||||
@@ -24,7 +24,7 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
sourceSets.androidTest.assets.srcDirs += ["../quartz/src/androidTest/assets"]
|
||||
sourceSets.androidTest.resources.srcDirs += ["../quartz/src/androidDeviceTest/resources"]
|
||||
|
||||
testBuildType = "benchmark"
|
||||
buildTypes {
|
||||
|
||||
+2
-4
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.benchmark
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.vitorpamplona.amethyst.commons.preview.MetaTagsParser
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
@@ -35,9 +34,8 @@ import java.nio.charset.Charset
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MetaTagsParserBenchmark {
|
||||
private val html =
|
||||
getInstrumentation()
|
||||
.context.assets
|
||||
.open("github_amethyst.html")
|
||||
javaClass.classLoader!!
|
||||
.getResourceAsStream("github_amethyst.html")
|
||||
.readBytes()
|
||||
.toString(Charset.forName("utf-8"))
|
||||
|
||||
|
||||
+1
-2
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -34,7 +33,7 @@ open class BaseLargeCacheBenchmark {
|
||||
companion object {
|
||||
fun getEventDB(): List<Event> {
|
||||
// This file includes duplicates
|
||||
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json")
|
||||
val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json")
|
||||
|
||||
return JsonMapper.mapper.readValue<ArrayList<Event>>(
|
||||
GZIPInputStream(fullDBInputStream),
|
||||
|
||||
@@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.benchmark
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -42,7 +41,7 @@ import java.util.zip.GZIPInputStream
|
||||
open class BaseCacheBenchmark {
|
||||
fun getEventDB(): List<Event> {
|
||||
// This file includes duplicates
|
||||
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json")
|
||||
val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json")
|
||||
|
||||
return JsonMapper.mapper.readValue<ArrayList<Event>>(
|
||||
GZIPInputStream(fullDBInputStream),
|
||||
|
||||
+3
-2
@@ -26,6 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.generateId
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.verifyId
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.assertTrue
|
||||
@@ -44,8 +45,8 @@ class EventCmdHasherBenchmark {
|
||||
@get:Rule val benchmarkRule = BenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun checkIDHashKind1WihtoutTags() {
|
||||
val event = JsonMapper.fromJson(JsonMapper.mapper.readTree(reqResponseEvent))
|
||||
fun checkIDHashKind1WithoutTags() {
|
||||
val event = EventMessage.parse(JsonMapper.mapper.readTree(reqResponseEvent)).event
|
||||
|
||||
benchmarkRule.measureRepeated {
|
||||
// Should pass
|
||||
|
||||
+2
-4
@@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.benchmark
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer
|
||||
@@ -47,9 +46,8 @@ class HintIndexerBenchmark {
|
||||
}
|
||||
|
||||
val relays =
|
||||
getInstrumentation()
|
||||
.context.assets
|
||||
.open("relayDB.txt")
|
||||
javaClass.classLoader
|
||||
?.getResourceAsStream("relayDB.txt")!!
|
||||
.readBytes()
|
||||
.toString(Charset.forName("utf-8"))
|
||||
.split("\n")
|
||||
|
||||
+13
-2
@@ -7,6 +7,8 @@ plugins {
|
||||
alias(libs.plugins.diffplugSpotless)
|
||||
alias(libs.plugins.googleServices) apply false
|
||||
alias(libs.plugins.jetbrainsComposeCompiler) apply false
|
||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||
alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -39,9 +41,18 @@ allprojects {
|
||||
|
||||
subprojects {
|
||||
afterEvaluate {
|
||||
tasks.named("preBuild") {
|
||||
dependsOn("spotlessApply")
|
||||
try {
|
||||
tasks.named("preBuild") {
|
||||
dependsOn("spotlessApply")
|
||||
}
|
||||
} catch (UnknownTaskException ignored) {
|
||||
tasks.matching {
|
||||
it.name.startsWith("pre") && it.name.endsWith("Build")
|
||||
}.configureEach {
|
||||
dependsOn("spotlessApply")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ composeCompiler {
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':quartz')
|
||||
|
||||
// Import @Immutable and @Stable
|
||||
implementation platform(libs.androidx.compose.bom)
|
||||
implementation libs.androidx.ui
|
||||
|
||||
Vendored
+3
-1
@@ -20,4 +20,6 @@
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep class com.vitorpamplona.amethyst.commons.** { *; }
|
||||
-keep class com.vitorpamplona.quartz.** { *; }
|
||||
-keep class com.vitorpamplona.amethyst.** { *; }
|
||||
-keep class com.vitorpamplona.ammolite.** { *; }
|
||||
@@ -54,6 +54,9 @@ zxing = "3.5.3"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
windowCoreAndroid = "1.4.0"
|
||||
androidxCamera = "1.4.2"
|
||||
kotlinStdlib = "2.2.10"
|
||||
kotlinTest = "2.2.10"
|
||||
core = "1.7.0"
|
||||
|
||||
[libraries]
|
||||
abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" }
|
||||
@@ -69,11 +72,12 @@ androidx-camera-extensions = { module = "androidx.camera:camera-extensions", ver
|
||||
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidxCamera" }
|
||||
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidxCamera" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
|
||||
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
||||
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
|
||||
androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxJunit" }
|
||||
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
|
||||
@@ -89,7 +93,6 @@ androidx-media3-session = { group = "androidx.media3", name = "media3-session",
|
||||
androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
|
||||
androidx-runtime-runtime = { group = "androidx.compose.runtime", name = "runtime" }
|
||||
androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "securityCryptoKtx" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
@@ -121,8 +124,12 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" }
|
||||
rfc3986-normalizer = { group = "org.czeal", name = "rfc3986", version.ref = "rfc3986" }
|
||||
secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", version.ref = "secp256k1KmpJniAndroid" }
|
||||
secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" }
|
||||
secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" }
|
||||
secp256k1-kmp-jni-jvm-linux = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm-linux", version.ref = "secp256k1KmpJniAndroid" }
|
||||
secp256k1-kmp-jni-jvm-darwin = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm-darwin", version.ref = "secp256k1KmpJniAndroid" }
|
||||
secp256k1-kmp-jni-jvm-mingw = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm-mingw", version.ref = "secp256k1KmpJniAndroid" }
|
||||
tor-android = { module = "info.guardianproject:tor-android", version.ref = "torAndroid" }
|
||||
unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" }
|
||||
url-detector = { group = "io.github.url-detector", name = "url-detector", version.ref = "urlDetector" }
|
||||
@@ -135,6 +142,9 @@ zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zooma
|
||||
zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
|
||||
zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
||||
androidx-window-core-android = { group = "androidx.window", name = "window-core-android", version.ref = "windowCoreAndroid" }
|
||||
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlinStdlib" }
|
||||
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlinTest" }
|
||||
androidx-core = { group = "androidx.test", name = "core", version.ref = "core" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
@@ -146,4 +156,6 @@ jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "k
|
||||
jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
jetbrainsComposeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
serialization = { id = 'org.jetbrains.kotlin.plugin.serialization', version.ref = 'kotlinxSerializationPlugin' }
|
||||
parcelize = { id = "kotlin-parcelize" }
|
||||
parcelize = { id = "kotlin-parcelize" }
|
||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
|
||||
@@ -1,88 +0,0 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
alias(libs.plugins.parcelize)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = 'com.vitorpamplona.quartz'
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInteger()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInteger()
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled = true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['**/libscrypt.dylib']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_21
|
||||
freeCompilerArgs.add("-Xstring-concat=inline")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation platform(libs.androidx.compose.bom)
|
||||
|
||||
// @Immutable and @Stable
|
||||
implementation libs.androidx.runtime.runtime
|
||||
|
||||
// Bitcoin secp256k1 bindings to Android
|
||||
api libs.secp256k1.kmp.jni.android
|
||||
|
||||
// LibSodium for ChaCha encryption (NIP-44)
|
||||
// Wait for @aar support in version catalogs
|
||||
|
||||
// implementation 'com.goterl:lazysodium-android:5.2.0@aar'
|
||||
// implementation 'net.java.dev.jna:jna:5.17.0@aar'
|
||||
|
||||
implementation variantOf(libs.lazysodium.android) { artifactType("aar") }
|
||||
implementation variantOf(libs.jna) { artifactType("aar") }
|
||||
|
||||
// Performant Parser of JSONs into Events
|
||||
api libs.jackson.module.kotlin
|
||||
|
||||
// immutable collections to avoid recomposition
|
||||
api libs.kotlinx.collections.immutable
|
||||
|
||||
// Parses URLs from Text:
|
||||
api libs.url.detector
|
||||
|
||||
// Normalizes URLs
|
||||
api libs.rfc3986.normalizer
|
||||
|
||||
// Websockets API
|
||||
implementation libs.okhttp
|
||||
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.secp256k1.kmp.jni.jvm
|
||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidKotlinMultiplatformLibrary)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
// Target declarations - add or remove as needed below. These define
|
||||
// which platforms this KMP module supports.
|
||||
// See: https://kotlinlang.org/docs/multiplatform-discover-project.html#targets
|
||||
androidLibrary {
|
||||
namespace = "com.vitorpamplona.quartz"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
|
||||
withHostTestBuilder {
|
||||
}
|
||||
|
||||
withDeviceTestBuilder {
|
||||
sourceSetTreeName = "test"
|
||||
}.configure {
|
||||
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
optimization {
|
||||
consumerKeepRules.publish = true
|
||||
consumerKeepRules.files.add(File(project.projectDir, "proguard-rules.pro"))
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes.add("**/libscrypt.dylib")
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xstring-concat=inline")
|
||||
}
|
||||
}
|
||||
|
||||
// For iOS targets, this is also where you should
|
||||
// configure native binary output. For more information, see:
|
||||
// https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#build-xcframeworks
|
||||
|
||||
// A step-by-step guide on how to include this library in an XCode
|
||||
// project can be found here:
|
||||
// https://developer.android.com/kotlin/multiplatform/migrate
|
||||
val xcfName = "quartz-kmpKit"
|
||||
|
||||
iosX64 {
|
||||
binaries.framework {
|
||||
baseName = xcfName
|
||||
}
|
||||
}
|
||||
|
||||
iosArm64 {
|
||||
binaries.framework {
|
||||
baseName = xcfName
|
||||
}
|
||||
}
|
||||
|
||||
iosSimulatorArm64 {
|
||||
binaries.framework {
|
||||
baseName = xcfName
|
||||
}
|
||||
}
|
||||
|
||||
// Source set declarations.
|
||||
// Declaring a target automatically creates a source set with the same name. By default, the
|
||||
// Kotlin Gradle Plugin creates additional source sets that depend on each other, since it is
|
||||
// common to share sources between related targets.
|
||||
// See: https://kotlinlang.org/docs/multiplatform-hierarchy.html
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(project.dependencies.platform(libs.androidx.compose.bom))
|
||||
|
||||
// @Immutable and @Stable
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
|
||||
api(libs.secp256k1.kmp.common)
|
||||
}
|
||||
}
|
||||
|
||||
jvmMain {
|
||||
dependencies {
|
||||
// Bitcoin secp256k1 bindings
|
||||
api(libs.secp256k1.kmp.jni.jvm)
|
||||
|
||||
// Performant Parser of JSONs into Events
|
||||
api(libs.jackson.module.kotlin)
|
||||
|
||||
// immutable collections to avoid recomposition
|
||||
api(libs.kotlinx.collections.immutable)
|
||||
|
||||
// Parses URLs from Text:
|
||||
api(libs.url.detector)
|
||||
|
||||
// Normalizes URLs
|
||||
api(libs.rfc3986.normalizer)
|
||||
|
||||
// Websockets API
|
||||
implementation(libs.okhttp)
|
||||
}
|
||||
}
|
||||
|
||||
commonTest {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
// Bitcoin secp256k1 bindings
|
||||
api(libs.secp256k1.kmp.jni.jvm)
|
||||
}
|
||||
}
|
||||
|
||||
androidMain {
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
|
||||
// @Immutable and @Stable
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
|
||||
// Bitcoin secp256k1 bindings to Android
|
||||
api(libs.secp256k1.kmp.jni.android)
|
||||
|
||||
// LibSodium for ChaCha encryption (NIP-44)
|
||||
implementation ("com.goterl:lazysodium-android:5.2.0@aar")
|
||||
implementation ("net.java.dev.jna:jna:5.17.0@aar")
|
||||
|
||||
// Performant Parser of JSONs into Events
|
||||
api(libs.jackson.module.kotlin)
|
||||
|
||||
// immutable collections to avoid recomposition
|
||||
api(libs.kotlinx.collections.immutable)
|
||||
|
||||
// Parses URLs from Text:
|
||||
api(libs.url.detector)
|
||||
|
||||
// Normalizes URLs
|
||||
api(libs.rfc3986.normalizer)
|
||||
|
||||
// Websockets API
|
||||
implementation(libs.okhttp)
|
||||
}
|
||||
}
|
||||
|
||||
getByName("androidDeviceTest") {
|
||||
dependencies {
|
||||
implementation(libs.androidx.runner)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.junit)
|
||||
implementation(libs.androidx.espresso.core)
|
||||
}
|
||||
}
|
||||
|
||||
iosMain {
|
||||
dependencies {
|
||||
// Add iOS-specific dependencies here. This a source set created by Kotlin Gradle
|
||||
// Plugin (KGP) that each specific iOS target (e.g., iosX64) depends on as
|
||||
// part of KMP’s default source set hierarchy. Note that this source set depends
|
||||
// on common by default and will correctly pull the iOS artifacts of any
|
||||
// KMP dependencies declared in commonMain.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "markdown",
|
||||
"source": "## Creating a new Kind 1 post"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"jupyter": {
|
||||
"is_executing": true
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"USE {\n",
|
||||
" dependencies(\"fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:0.17.3\")\n",
|
||||
" dependencies(\"com.goterl:lazysodium-java:5.1.4\")\n",
|
||||
" dependencies(\"net.java.dev.jna:jna:5.17.0\")\n",
|
||||
"\n",
|
||||
" import(\"com.vitorpamplona.quartz.*\")\n",
|
||||
"}"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"collapsed": true,
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-06-03T14:42:40.051027Z",
|
||||
"start_time": "2025-06-03T14:42:40.023052Z"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"import fr.acinq.secp256k1.Secp256k1\n",
|
||||
"\n",
|
||||
"import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair\n",
|
||||
"import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal\n",
|
||||
"import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync\n",
|
||||
"import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent\n",
|
||||
"\n",
|
||||
"val signer = NostrSignerSync()\n",
|
||||
"val kind1Template = TextNoteEvent.build(\"New kind 1 Post\")\n",
|
||||
"val signedEvent = signer.sign(kind1Template)\n",
|
||||
"\n",
|
||||
"println(signedEvent)"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException",
|
||||
"evalue": "at Cell In[5], line 3, column 43: Unresolved reference: crypto\nat Cell In[5], line 4, column 43: Unresolved reference: signers\nat Cell In[5], line 5, column 43: Unresolved reference: signers\nat Cell In[5], line 6, column 33: Unresolved reference: nip10Notes\nat Cell In[5], line 8, column 14: Unresolved reference: NostrSignerSync\nat Cell In[5], line 9, column 21: Unresolved reference: TextNoteEvent\nat Cell In[5], line 12, column 1: Overload resolution ambiguity: \npublic inline fun println(message: Any?): Unit defined in kotlin.io\npublic inline fun println(message: Boolean): Unit defined in kotlin.io\npublic inline fun println(message: Byte): Unit defined in kotlin.io\npublic inline fun println(message: Char): Unit defined in kotlin.io\npublic inline fun println(message: CharArray): Unit defined in kotlin.io\npublic inline fun println(message: Double): Unit defined in kotlin.io\npublic inline fun println(message: Float): Unit defined in kotlin.io\npublic inline fun println(message: Int): Unit defined in kotlin.io\npublic inline fun println(message: Long): Unit defined in kotlin.io\npublic inline fun println(message: Short): Unit defined in kotlin.io",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException: at Cell In[5], line 3, column 43: Unresolved reference: crypto",
|
||||
"at Cell In[5], line 4, column 43: Unresolved reference: signers",
|
||||
"at Cell In[5], line 5, column 43: Unresolved reference: signers",
|
||||
"at Cell In[5], line 6, column 33: Unresolved reference: nip10Notes",
|
||||
"at Cell In[5], line 8, column 14: Unresolved reference: NostrSignerSync",
|
||||
"at Cell In[5], line 9, column 21: Unresolved reference: TextNoteEvent",
|
||||
"at Cell In[5], line 12, column 1: Overload resolution ambiguity: ",
|
||||
"public inline fun println(message: Any?): Unit defined in kotlin.io",
|
||||
"public inline fun println(message: Boolean): Unit defined in kotlin.io",
|
||||
"public inline fun println(message: Byte): Unit defined in kotlin.io",
|
||||
"public inline fun println(message: Char): Unit defined in kotlin.io",
|
||||
"public inline fun println(message: CharArray): Unit defined in kotlin.io",
|
||||
"public inline fun println(message: Double): Unit defined in kotlin.io",
|
||||
"public inline fun println(message: Float): Unit defined in kotlin.io",
|
||||
"public inline fun println(message: Int): Unit defined in kotlin.io",
|
||||
"public inline fun println(message: Long): Unit defined in kotlin.io",
|
||||
"public inline fun println(message: Short): Unit defined in kotlin.io",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.JupyterCompilerImpl.compileSync(JupyterCompilerImpl.kt:208)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.InternalEvaluatorImpl.eval(InternalEvaluatorImpl.kt:126)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl$execute$1$result$1.invoke(CellExecutorImpl.kt:80)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl$execute$1$result$1.invoke(CellExecutorImpl.kt:78)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.withHost(ReplForJupyterImpl.kt:791)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl.execute-L4Nmkdk(CellExecutorImpl.kt:78)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.execution.CellExecutor$DefaultImpls.execute-L4Nmkdk$default(CellExecutor.kt:13)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.evaluateUserCode-wNURfNM(ReplForJupyterImpl.kt:613)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.evalExImpl(ReplForJupyterImpl.kt:471)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.access$evalExImpl(ReplForJupyterImpl.kt:143)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl$evalEx$1.invoke(ReplForJupyterImpl.kt:464)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl$evalEx$1.invoke(ReplForJupyterImpl.kt:463)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.withEvalContext(ReplForJupyterImpl.kt:444)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.evalEx(ReplForJupyterImpl.kt:463)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1$1.invoke(IdeCompatibleMessageRequestProcessor.kt:159)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1$1.invoke(IdeCompatibleMessageRequestProcessor.kt:158)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.streams.BlockingSubstitutionEngine.withDataSubstitution(SubstitutionEngine.kt:70)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.streams.StreamSubstitutionManager.withSubstitutedStreams(StreamSubstitutionManager.kt:118)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.withForkedIn(IdeCompatibleMessageRequestProcessor.kt:335)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.access$withForkedIn(IdeCompatibleMessageRequestProcessor.kt:54)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$evalWithIO$1$1.invoke(IdeCompatibleMessageRequestProcessor.kt:349)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.streams.BlockingSubstitutionEngine.withDataSubstitution(SubstitutionEngine.kt:70)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.streams.StreamSubstitutionManager.withSubstitutedStreams(StreamSubstitutionManager.kt:118)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.withForkedErr(IdeCompatibleMessageRequestProcessor.kt:324)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.access$withForkedErr(IdeCompatibleMessageRequestProcessor.kt:54)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$evalWithIO$1.invoke(IdeCompatibleMessageRequestProcessor.kt:348)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.streams.BlockingSubstitutionEngine.withDataSubstitution(SubstitutionEngine.kt:70)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.streams.StreamSubstitutionManager.withSubstitutedStreams(StreamSubstitutionManager.kt:118)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.withForkedOut(IdeCompatibleMessageRequestProcessor.kt:316)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.evalWithIO(IdeCompatibleMessageRequestProcessor.kt:347)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1.invoke(IdeCompatibleMessageRequestProcessor.kt:158)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1.invoke(IdeCompatibleMessageRequestProcessor.kt:157)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$Task.execute(JupyterExecutorImpl.kt:41)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$executorThread$1.invoke(JupyterExecutorImpl.kt:83)",
|
||||
"\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$executorThread$1.invoke(JupyterExecutorImpl.kt:80)",
|
||||
"\tat kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)",
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 5
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"source": "",
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Kotlin",
|
||||
"language": "kotlin",
|
||||
"name": "kotlin"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "kotlin",
|
||||
"version": "1.9.23",
|
||||
"mimetype": "text/x-kotlin",
|
||||
"file_extension": ".kt",
|
||||
"pygments_lexer": "kotlin",
|
||||
"codemirror_mode": "text/x-kotlin",
|
||||
"nbconvert_exporter": ""
|
||||
},
|
||||
"ktnbPluginMetadata": {
|
||||
"projectDependencies": [
|
||||
"Amethyst.amethyst.unitTest"
|
||||
],
|
||||
"projectLibraries": false
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
Vendored
+2
-1
@@ -58,4 +58,5 @@
|
||||
}
|
||||
|
||||
# JSON parsing
|
||||
-keep class com.vitorpamplona.quartz.** { *; }
|
||||
-keep class com.vitorpamplona.quartz.** { *; }
|
||||
|
||||
|
||||
+2
-3
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.quartz
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
@@ -39,7 +38,7 @@ class LargeDBSignatureCheck {
|
||||
@Test
|
||||
fun insertDatabaseSample() =
|
||||
runBlocking {
|
||||
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_short.json")
|
||||
val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_short.json")
|
||||
|
||||
val eventArray =
|
||||
JsonMapper.mapper.readValue<ArrayList<Event>>(
|
||||
@@ -59,7 +58,7 @@ class LargeDBSignatureCheck {
|
||||
fun insertStartupDatabase() =
|
||||
runBlocking {
|
||||
// This file includes duplicates
|
||||
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json")
|
||||
val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json")
|
||||
|
||||
val eventArray =
|
||||
JsonMapper.mapper.readValue<ArrayList<Event>>(
|
||||
+1
-2
@@ -25,7 +25,6 @@ import android.database.sqlite.SQLiteException
|
||||
import android.util.Log
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
@@ -42,7 +41,7 @@ class LargeDBTests {
|
||||
companion object {
|
||||
fun getEventDB(): List<Event> {
|
||||
// This file includes duplicates
|
||||
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json")
|
||||
val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json")
|
||||
|
||||
return JsonMapper.mapper.readValue<ArrayList<Event>>(
|
||||
GZIPInputStream(fullDBInputStream),
|
||||
-11
@@ -29,10 +29,7 @@ import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.fail
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class OtsTest {
|
||||
val resolver = OtsResolver()
|
||||
@@ -75,12 +72,8 @@ class OtsTest {
|
||||
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
val newOts = runBlocking { signer.sign(OtsEvent.build(eventId, upgraded!!)) }
|
||||
|
||||
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
|
||||
println(newOts.toJson())
|
||||
println(resolver.info(newOts.otsByteArray()))
|
||||
|
||||
@@ -91,15 +84,11 @@ class OtsTest {
|
||||
fun createOTSEventAndVerify() {
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
val ots =
|
||||
runBlocking {
|
||||
signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest, resolver)))
|
||||
}
|
||||
|
||||
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
|
||||
println(ots.toJson())
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
|
||||
+1
-7
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.quartz.nip06KeyDerivation
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
@@ -36,12 +35,7 @@ import org.junit.runner.RunWith
|
||||
class Bip39MnemonicsTest {
|
||||
private val tests =
|
||||
jacksonObjectMapper()
|
||||
.readTree(
|
||||
InstrumentationRegistry
|
||||
.getInstrumentation()
|
||||
.context.assets
|
||||
.open("bip39.vectors.json"),
|
||||
)
|
||||
.readTree(javaClass.classLoader?.getResourceAsStream("bip39.vectors.json"))
|
||||
|
||||
@Test
|
||||
fun toSeed() {
|
||||
+4
-5
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.quartz.nip17Dm
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
||||
import junit.framework.TestCase.assertEquals
|
||||
@@ -46,9 +45,9 @@ class AESGCMTest {
|
||||
@Test
|
||||
fun imageTest() {
|
||||
val image =
|
||||
getInstrumentation().context.assets.open("ovxxk2vz.jpg").use {
|
||||
javaClass.classLoader?.getResourceAsStream("ovxxk2vz.jpg")?.use {
|
||||
it.readAllBytes()
|
||||
}
|
||||
}!!
|
||||
|
||||
val decrypted = cipher.decrypt(image)
|
||||
|
||||
@@ -64,9 +63,9 @@ class AESGCMTest {
|
||||
)
|
||||
|
||||
val encrypted =
|
||||
getInstrumentation().context.assets.open("trouble_video").use {
|
||||
javaClass.classLoader?.getResourceAsStream("trouble_video")?.use {
|
||||
it.readAllBytes()
|
||||
}
|
||||
}!!
|
||||
|
||||
val decrypted = myCipher.decrypt(encrypted)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user