- Restructures the old static datasource model into dynamic filter assemblers.
- Moves filter assemblers, viewModels and DAL classes to their own packages. - Creates Composable observers for Users and Notes - Deletes most of the secondary LiveData objects in the move to Flow - Manipulates nostr filters depending on the account of the current screen, not a global account. - Unifies all FilterAssembly lifecycle watchers to a few classes. - Prepares to separate The Nostr Client as an Engine of Amethyst. - Moves the pre-caching processor of new events from the datasource to the accountViewModel - Reorganizes search to be per-screen basis - Moves authentication to a fixed Coordinator class for all accounts in all relays. - Moves NOTIFY command to its own coordinator class for all accounts - Moves the connection between filters and cache to its own class. - Significantly reduces the dependency on a single ServiceManager class.
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
||||
|
||||
@@ -26,7 +26,9 @@ import android.util.Log
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.memory.MemoryCache
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
||||
import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService
|
||||
import com.vitorpamplona.amethyst.service.images.ImageCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
@@ -38,6 +40,10 @@ import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
|
||||
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
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
@@ -53,6 +59,7 @@ import java.io.File
|
||||
class Amethyst : Application() {
|
||||
val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}"
|
||||
|
||||
// Exists to avoid exceptions stopping the coroutine
|
||||
val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable)
|
||||
@@ -60,7 +67,7 @@ class Amethyst : Application() {
|
||||
|
||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
|
||||
|
||||
// Key cache to download and decrypt encrypted files before caching them.
|
||||
// Key cache service to download and decrypt encrypted files before caching them.
|
||||
val keyCache = EncryptionKeyCache()
|
||||
|
||||
// App services that should be run as soon as there are subscribers to their flows
|
||||
@@ -68,9 +75,10 @@ class Amethyst : Application() {
|
||||
val torManager = TorManager(this, applicationIOScope)
|
||||
val connManager = ConnectivityManager(this, applicationIOScope)
|
||||
|
||||
// Service that will run at all times.
|
||||
// Service that will run at all times to receive events from Pokey
|
||||
val pokeyReceiver = PokeyReceiver()
|
||||
|
||||
// creates okHttpClients based on the conditions of the connection and tor status
|
||||
val okHttpClients =
|
||||
DualHttpClientManager(
|
||||
userAgent = appAgent,
|
||||
@@ -80,21 +88,52 @@ class Amethyst : Application() {
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// Connects the NostrClient class with okHttp
|
||||
val factory =
|
||||
OkHttpWebSocket.BuilderFactory { _, useProxy ->
|
||||
okHttpClients.getHttpClient(useProxy)
|
||||
}
|
||||
|
||||
// Provides a relay pool
|
||||
val client: NostrClient = NostrClient(factory)
|
||||
|
||||
// Caches all events in Memory
|
||||
val cache: LocalCache = LocalCache
|
||||
|
||||
// Verifies and inserts in the cache from all relays, all subscriptions
|
||||
val cacheClientConnector = CacheClientConnector(client, cache)
|
||||
|
||||
// Show messages from the Relay and controls their dismissal
|
||||
val notifyCoordinator = NotifyCoordinator(client)
|
||||
|
||||
// Authenticates with relays.
|
||||
val authCoordinator = AuthCoordinator(client)
|
||||
|
||||
// Organizes cache clearing
|
||||
val trimmingService = MemoryTrimmingService(cache)
|
||||
|
||||
// Coordinates all subscriptions for the Nostr Client
|
||||
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope)
|
||||
|
||||
// Trash.
|
||||
val serviceManager = ServiceManager(client, applicationIOScope)
|
||||
|
||||
// saves the .content of NIP-95 blobs in disk to save memory
|
||||
val nip95cache: File by lazy { Nip95CacheFactory.new(this) }
|
||||
|
||||
// local video cache with disk + memory
|
||||
val videoCache: VideoCache by lazy { VideoCacheFactory.new(this) }
|
||||
|
||||
// image cache in disk for coil
|
||||
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(this) }
|
||||
|
||||
// image cache in memory for coil
|
||||
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(this) }
|
||||
|
||||
// Application-wide ots verification cache
|
||||
val otsVerifCache by lazy { VerificationStateCache() }
|
||||
|
||||
// Application-wide block height request cache
|
||||
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -103,7 +142,7 @@ class Amethyst : Application() {
|
||||
|
||||
instance = this
|
||||
|
||||
if (isDebug()) {
|
||||
if (isDebug) {
|
||||
Logging.setup()
|
||||
}
|
||||
|
||||
@@ -124,10 +163,8 @@ class Amethyst : Application() {
|
||||
|
||||
fun contentResolverFn(): ContentResolver = contentResolver
|
||||
|
||||
fun isDebug() = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
|
||||
|
||||
fun setImageLoader(shouldUseTor: Boolean?) =
|
||||
ImageLoaderSetup.setup(this, diskCache, memoryCache, isDebug()) {
|
||||
ImageLoaderSetup.setup(this, diskCache, memoryCache) {
|
||||
shouldUseTor?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false)
|
||||
}
|
||||
|
||||
@@ -142,7 +179,7 @@ class Amethyst : Application() {
|
||||
super.onTrimMemory(level)
|
||||
Log.d("AmethystApp", "onTrimMemory $level")
|
||||
applicationIOScope.launch(Dispatchers.Default) {
|
||||
serviceManager.trimMemory()
|
||||
trimmingService.run(null, LocalPreferences.allSavedAccounts())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,45 +27,15 @@ import android.os.Debug
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
|
||||
|
||||
fun debugState(context: Context) {
|
||||
Amethyst.instance.client
|
||||
.allSubscriptions()
|
||||
.forEach { Log.d("STATE DUMP", "${it.key} ${it.value.joinToString { it.filter.toDebugJson() }}") }
|
||||
|
||||
NostrAccountDataSource.printCounter()
|
||||
NostrChannelDataSource.printCounter()
|
||||
NostrChatroomDataSource.printCounter()
|
||||
NostrChatroomListDataSource.printCounter()
|
||||
NostrCommunityDataSource.printCounter()
|
||||
NostrDiscoveryDataSource.printCounter()
|
||||
NostrHashtagDataSource.printCounter()
|
||||
NostrGeohashDataSource.printCounter()
|
||||
NostrHomeDataSource.printCounter()
|
||||
NostrSearchEventOrUserDataSource.printCounter()
|
||||
NostrSingleChannelDataSource.printCounter()
|
||||
NostrSingleEventDataSource.printCounter()
|
||||
NostrSingleUserDataSource.printCounter()
|
||||
NostrThreadDataSource.printCounter()
|
||||
NostrUserProfileDataSource.printCounter()
|
||||
NostrVideoDataSource.printCounter()
|
||||
|
||||
val totalMemoryMb = Runtime.getRuntime().totalMemory() / (1024 * 1024)
|
||||
val freeMemoryMb = Runtime.getRuntime().freeMemory() / (1024 * 1024)
|
||||
val maxMemoryMb = Runtime.getRuntime().maxMemory() / (1024 * 1024)
|
||||
@@ -174,3 +144,27 @@ fun debugState(context: Context) {
|
||||
Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables.get(kind)?.div((1024 * 1024))}MB ")
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> logTime(
|
||||
debugMessage: String,
|
||||
block: () -> T,
|
||||
): T =
|
||||
if (isDebug) {
|
||||
val (result, elapsed) = measureTimedValue(block)
|
||||
Log.d("DEBUG-TIME", "$elapsed: $debugMessage")
|
||||
result
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
|
||||
inline fun <T> logTime(
|
||||
debugMessage: (T) -> Unit,
|
||||
block: () -> T,
|
||||
): T =
|
||||
if (isDebug) {
|
||||
val (result, elapsed) = measureTimedValue(block)
|
||||
Log.d("DEBUG-TIME", "$elapsed: ${debugMessage(result)}")
|
||||
result
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
|
||||
@@ -25,35 +25,12 @@ import androidx.compose.runtime.Stable
|
||||
import coil3.annotation.DelicateCoilApi
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@Stable
|
||||
class ServiceManager(
|
||||
@@ -67,8 +44,6 @@ class ServiceManager(
|
||||
|
||||
private var collectorJob: Job? = null
|
||||
|
||||
private val trimmingService = MemoryTrimmingService()
|
||||
|
||||
private fun start(account: Account) {
|
||||
this.account = account
|
||||
start()
|
||||
@@ -104,39 +79,6 @@ class ServiceManager(
|
||||
}
|
||||
}
|
||||
|
||||
// start services
|
||||
NostrAccountDataSource.account = myAccount
|
||||
NostrHomeDataSource.account = myAccount
|
||||
NostrChatroomListDataSource.account = myAccount
|
||||
NostrVideoDataSource.account = myAccount
|
||||
NostrDiscoveryDataSource.account = myAccount
|
||||
|
||||
NostrAccountDataSource.otherAccounts =
|
||||
runBlocking {
|
||||
LocalPreferences.allSavedAccounts().mapNotNull {
|
||||
try {
|
||||
it.npub.bechToBytes().toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification Elements
|
||||
NostrHomeDataSource.start()
|
||||
NostrAccountDataSource.start()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
delay(3000)
|
||||
NostrChatroomListDataSource.start()
|
||||
NostrDiscoveryDataSource.start()
|
||||
NostrVideoDataSource.start()
|
||||
}
|
||||
|
||||
// More Info Data Sources
|
||||
NostrSingleEventDataSource.start()
|
||||
NostrSingleChannelDataSource.start()
|
||||
NostrSingleUserDataSource.start()
|
||||
isStarted = true
|
||||
}
|
||||
}
|
||||
@@ -147,24 +89,6 @@ class ServiceManager(
|
||||
collectorJob?.cancel()
|
||||
collectorJob = null
|
||||
|
||||
NostrAccountDataSource.stopSync()
|
||||
NostrHomeDataSource.stopSync()
|
||||
NostrChannelDataSource.stopSync()
|
||||
NostrChatroomDataSource.stopSync()
|
||||
NostrChatroomListDataSource.stopSync()
|
||||
NostrDiscoveryDataSource.stopSync()
|
||||
|
||||
NostrCommunityDataSource.stopSync()
|
||||
NostrHashtagDataSource.stopSync()
|
||||
NostrGeohashDataSource.stopSync()
|
||||
NostrSearchEventOrUserDataSource.stopSync()
|
||||
NostrSingleChannelDataSource.stopSync()
|
||||
NostrSingleEventDataSource.stopSync()
|
||||
NostrSingleUserDataSource.stopSync()
|
||||
NostrThreadDataSource.stopSync()
|
||||
NostrUserProfileDataSource.stopSync()
|
||||
NostrVideoDataSource.stopSync()
|
||||
|
||||
client.reconnect(null)
|
||||
isStarted = false
|
||||
}
|
||||
@@ -173,10 +97,6 @@ class ServiceManager(
|
||||
LocalCache.cleanObservers()
|
||||
}
|
||||
|
||||
suspend fun trimMemory() {
|
||||
trimmingService.run(account)
|
||||
}
|
||||
|
||||
// This method keeps the pause/start in a Syncronized block to
|
||||
// avoid concurrent pauses and starts.
|
||||
@Synchronized
|
||||
|
||||
@@ -33,10 +33,10 @@ import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState
|
||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||
import com.vitorpamplona.amethyst.tryAndWait
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
@@ -220,14 +220,6 @@ class Account(
|
||||
|
||||
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
|
||||
|
||||
data class PaymentRequest(
|
||||
val relayUrl: String,
|
||||
val description: String,
|
||||
)
|
||||
|
||||
var transientPaymentRequestDismissals: Set<PaymentRequest> = emptySet()
|
||||
val transientPaymentRequests: MutableStateFlow<Set<PaymentRequest>> = MutableStateFlow(emptySet())
|
||||
|
||||
@Immutable
|
||||
class LiveFollowList(
|
||||
val authors: Set<String> = emptySet(),
|
||||
@@ -1101,7 +1093,7 @@ class Account(
|
||||
val liveHiddenUsers = flowHiddenUsers.asLiveData()
|
||||
|
||||
val decryptBookmarks: LiveData<BookmarkListEvent?> by lazy {
|
||||
userProfile().live().innerBookmarks.switchMap { userState ->
|
||||
userProfile().live().bookmarks.switchMap { userState ->
|
||||
liveData(Dispatchers.IO) {
|
||||
if (userState.user.latestBookmarkList == null) {
|
||||
emit(null)
|
||||
@@ -1190,22 +1182,6 @@ class Account(
|
||||
)
|
||||
}
|
||||
|
||||
fun addPaymentRequestIfNew(paymentRequest: PaymentRequest) {
|
||||
if (
|
||||
!this.transientPaymentRequests.value.contains(paymentRequest) &&
|
||||
!this.transientPaymentRequestDismissals.contains(paymentRequest)
|
||||
) {
|
||||
this.transientPaymentRequests.value += paymentRequest
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissPaymentRequest(request: PaymentRequest) {
|
||||
if (this.transientPaymentRequests.value.contains(request)) {
|
||||
this.transientPaymentRequests.value -= request
|
||||
this.transientPaymentRequestDismissals += request
|
||||
}
|
||||
}
|
||||
|
||||
private var userProfileCache: User? = null
|
||||
|
||||
fun userProfile(): User = userProfileCache ?: LocalCache.getOrCreateUser(signer.pubKey).also { userProfileCache = it }
|
||||
@@ -1564,14 +1540,15 @@ class Account(
|
||||
nip47.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) } ?: signer
|
||||
|
||||
LnZapPaymentRequestEvent.create(bolt11, nip47.pubKeyHex, signer) { event ->
|
||||
val wcListener =
|
||||
NostrLnZapPaymentResponseDataSource(
|
||||
val filter =
|
||||
NWCPaymentQueryState(
|
||||
fromServiceHex = nip47.pubKeyHex,
|
||||
toUserHex = event.pubKey,
|
||||
replyingToHex = event.id,
|
||||
authSigner = signer,
|
||||
)
|
||||
wcListener.startSync()
|
||||
|
||||
Amethyst.instance.sources.nwc
|
||||
.subscribe(filter)
|
||||
|
||||
LocalCache.consume(event, zappedNote) { it.response(signer) { onResponse(it) } }
|
||||
|
||||
@@ -1583,9 +1560,12 @@ class Account(
|
||||
forceProxy = shouldUseTorForTrustedRelays(), // this is trusted.
|
||||
read = true,
|
||||
write = true,
|
||||
feedTypes = wcListener.feedTypes,
|
||||
feedTypes = setOf(FeedType.WALLET_CONNECT),
|
||||
),
|
||||
onDone = { wcListener.destroy() },
|
||||
onDone = {
|
||||
Amethyst.instance.sources.nwc
|
||||
.unsubscribe(filter)
|
||||
},
|
||||
)
|
||||
|
||||
onSent()
|
||||
|
||||
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.commons.data.LargeCache
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
@@ -230,12 +229,11 @@ abstract class Channel(
|
||||
// Observers line up here.
|
||||
val live: ChannelLiveData = ChannelLiveData(this)
|
||||
|
||||
fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
|
||||
fun pruneOldMessages(): Set<Note> {
|
||||
val important =
|
||||
notes
|
||||
.filter { key, it ->
|
||||
it.author?.let { author -> account.isHidden(author) } == false
|
||||
}.sortedWith(DefaultFeedOrder)
|
||||
.values()
|
||||
.sortedWith(DefaultFeedOrder)
|
||||
.take(500)
|
||||
.toSet()
|
||||
|
||||
@@ -245,6 +243,18 @@ abstract class Channel(
|
||||
|
||||
return toBeRemoved.toSet()
|
||||
}
|
||||
|
||||
fun pruneHiddenMessages(account: Account): Set<Note> {
|
||||
val hidden =
|
||||
notes
|
||||
.filter { key, it ->
|
||||
it.author?.let { author -> account.isHidden(author) } == true
|
||||
}.toSet()
|
||||
|
||||
hidden.forEach { notes.remove(it.idHex) }
|
||||
|
||||
return hidden.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelLiveData(
|
||||
@@ -263,16 +273,6 @@ class ChannelLiveData(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
NostrSingleChannelDataSource.add(channel)
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
NostrSingleChannelDataSource.remove(channel)
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelState(
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.commons.data.DeletionIndex
|
||||
import com.vitorpamplona.amethyst.commons.data.LargeCache
|
||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor
|
||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource.account
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.ammolite.relays.BundledInsert
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
@@ -171,7 +170,26 @@ import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object LocalCache {
|
||||
interface ILocalCache {
|
||||
fun verifyAndConsume(
|
||||
event: Event,
|
||||
relay: Relay?,
|
||||
)
|
||||
|
||||
fun justVerify(event: Event): Boolean
|
||||
|
||||
fun consume(
|
||||
event: DraftEvent,
|
||||
relay: Relay?,
|
||||
)
|
||||
|
||||
fun markAsSeen(
|
||||
string: String,
|
||||
relay: Relay,
|
||||
) {}
|
||||
}
|
||||
|
||||
object LocalCache : ILocalCache {
|
||||
val antiSpam = AntiSpamFilter()
|
||||
|
||||
val users = LargeCache<HexKey, User>()
|
||||
@@ -185,6 +203,8 @@ object LocalCache {
|
||||
val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>>(10)
|
||||
val observablesByKindAndAuthor = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindAndAuthor<Event>>>(10)
|
||||
|
||||
val onNewEvents = mutableListOf<(Note) -> Unit>()
|
||||
|
||||
fun <T : Event> observeETag(
|
||||
kind: Int,
|
||||
eventId: HexKey,
|
||||
@@ -452,6 +472,7 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex()} ${oldUser.toBestDisplayName()} from ${relay?.url}")
|
||||
} else {
|
||||
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()}
|
||||
@@ -917,7 +938,7 @@ object LocalCache {
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
author.liveSet?.innerStatuses?.invalidateData()
|
||||
author.liveSet?.statuses?.invalidateData()
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
@@ -942,7 +963,7 @@ object LocalCache {
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.liveSet?.innerOts?.invalidateData()
|
||||
version.liveSet?.ots?.invalidateData()
|
||||
}
|
||||
|
||||
refreshObservers(version)
|
||||
@@ -1358,7 +1379,7 @@ object LocalCache {
|
||||
|
||||
mentions.forEach {
|
||||
// doesn't add to reports, but triggers recounts
|
||||
it.liveSet?.innerReports?.invalidateData()
|
||||
it.liveSet?.reports?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1604,7 +1625,7 @@ object LocalCache {
|
||||
checkGetOrCreateNote(it.eventId)?.let { editedNote ->
|
||||
modificationCache.remove(editedNote.idHex)
|
||||
// must update list of Notes to quickly update the user.
|
||||
editedNote.liveSet?.innerModifications?.invalidateData()
|
||||
editedNote.liveSet?.edits?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1832,6 +1853,8 @@ object LocalCache {
|
||||
username: String,
|
||||
forAccount: Account?,
|
||||
): List<User> {
|
||||
if (username.isBlank()) return emptyList()
|
||||
|
||||
checkNotInMainThread()
|
||||
|
||||
val key = decodePublicKeyAsHexOrNull(username)
|
||||
@@ -1884,6 +1907,8 @@ object LocalCache {
|
||||
): List<Note> {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (text.isBlank()) return emptyList()
|
||||
|
||||
val key = decodeEventIdAsHexOrNull(text)
|
||||
|
||||
if (key != null) {
|
||||
@@ -1948,6 +1973,8 @@ object LocalCache {
|
||||
fun findChannelsStartingWith(text: String): List<Channel> {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (text.isBlank()) return emptyList()
|
||||
|
||||
val key = decodeEventIdAsHexOrNull(text)
|
||||
if (key != null && getChannelIfExists(key) != null) {
|
||||
return listOfNotNull(getChannelIfExists(key))
|
||||
@@ -2054,11 +2081,9 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneOldAndHiddenMessages(account: Account) {
|
||||
checkNotInMainThread()
|
||||
|
||||
fun pruneHiddenMessages(account: Account) {
|
||||
channels.forEach { _, channel ->
|
||||
val toBeRemoved = channel.pruneOldAndHiddenMessages(account)
|
||||
val toBeRemoved = channel.pruneHiddenMessages(account)
|
||||
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
@@ -2072,7 +2097,31 @@ object LocalCache {
|
||||
|
||||
if (toBeRemoved.size > 100 || channel.notes.size() > 100) {
|
||||
println(
|
||||
"PRUNE: ${toBeRemoved.size} messages removed from ${channel.toBestDisplayName()}. ${channel.notes.size()} kept",
|
||||
"PRUNE: ${toBeRemoved.size} hidden messages removed from ${channel.toBestDisplayName()}. ${channel.notes.size()} kept",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneOldMessages() {
|
||||
checkNotInMainThread()
|
||||
|
||||
channels.forEach { _, channel ->
|
||||
val toBeRemoved = channel.pruneOldMessages()
|
||||
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
removeFromCache(it)
|
||||
|
||||
childrenToBeRemoved.addAll(it.removeAllChildNotes())
|
||||
}
|
||||
|
||||
removeFromCache(childrenToBeRemoved)
|
||||
|
||||
if (toBeRemoved.size > 100 || channel.notes.size() > 100) {
|
||||
println(
|
||||
"PRUNE: ${toBeRemoved.size} old messages removed from ${channel.toBestDisplayName()}. ${channel.notes.size()} kept",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2258,7 +2307,7 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneHiddenMessages(account: Account) {
|
||||
fun pruneHiddenEvents(account: Account) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
@@ -2299,15 +2348,32 @@ object LocalCache {
|
||||
println("PRUNE: $removingContactList contact lists")
|
||||
}
|
||||
|
||||
override fun markAsSeen(
|
||||
eventId: String,
|
||||
relay: Relay,
|
||||
) {
|
||||
val note = getNoteIfExists(eventId)
|
||||
|
||||
note?.event?.let { noteEvent ->
|
||||
if (noteEvent is AddressableEvent) {
|
||||
getAddressableNoteIfExists(noteEvent.aTag().toTag())?.addRelay(relay)
|
||||
}
|
||||
}
|
||||
|
||||
note?.addRelay(relay)
|
||||
}
|
||||
|
||||
// Observers line up here.
|
||||
val live: LocalCacheFlow = LocalCacheFlow()
|
||||
|
||||
private fun refreshObservers(newNote: Note) {
|
||||
updateObservables(newNote.event as Event)
|
||||
val event = newNote.event as Event
|
||||
updateObservables(event)
|
||||
onNewEvents.forEach { it(newNote) }
|
||||
live.invalidateData(newNote)
|
||||
}
|
||||
|
||||
fun verifyAndConsume(
|
||||
override fun verifyAndConsume(
|
||||
event: Event,
|
||||
relay: Relay?,
|
||||
) {
|
||||
@@ -2316,7 +2382,7 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun justVerify(event: Event): Boolean {
|
||||
override fun justVerify(event: Event): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
return if (!event.verify()) {
|
||||
@@ -2332,7 +2398,7 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(
|
||||
override fun consume(
|
||||
event: DraftEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
@@ -2639,6 +2705,16 @@ object LocalCache {
|
||||
val note = notes.get(notificationEvent.id)
|
||||
note?.event != null
|
||||
}
|
||||
|
||||
fun copyRelaysFromTo(
|
||||
from: Note,
|
||||
to: Event,
|
||||
) {
|
||||
val toNote = getOrCreateNote(to)
|
||||
from.relays.forEach {
|
||||
toNote.addRelayBrief(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
||||
@@ -23,15 +23,11 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import com.vitorpamplona.amethyst.launchAndWaitAll
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
import com.vitorpamplona.amethyst.service.replace
|
||||
import com.vitorpamplona.amethyst.tryAndWait
|
||||
import com.vitorpamplona.amethyst.ui.note.combineWith
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
@@ -232,7 +228,7 @@ open class Note(
|
||||
this.author = author
|
||||
this.replyTo = replyTo
|
||||
|
||||
liveSet?.innerMetadata?.invalidateData()
|
||||
liveSet?.metadata?.invalidateData()
|
||||
flowSet?.metadata?.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -240,21 +236,21 @@ open class Note(
|
||||
fun addReply(note: Note) {
|
||||
if (note !in replies) {
|
||||
replies = replies + note
|
||||
liveSet?.innerReplies?.invalidateData()
|
||||
liveSet?.replies?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeReply(note: Note) {
|
||||
if (note in replies) {
|
||||
replies = replies - note
|
||||
liveSet?.innerReplies?.invalidateData()
|
||||
liveSet?.replies?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeBoost(note: Note) {
|
||||
if (note in boosts) {
|
||||
boosts = boosts - note
|
||||
liveSet?.innerBoosts?.invalidateData()
|
||||
liveSet?.boosts?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,13 +281,13 @@ open class Note(
|
||||
relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
||||
lastReactionsDownloadTime = emptyMap()
|
||||
|
||||
if (repliesChanged) liveSet?.innerReplies?.invalidateData()
|
||||
if (reactionsChanged) liveSet?.innerReactions?.invalidateData()
|
||||
if (boostsChanged) liveSet?.innerBoosts?.invalidateData()
|
||||
if (repliesChanged) liveSet?.replies?.invalidateData()
|
||||
if (reactionsChanged) liveSet?.reactions?.invalidateData()
|
||||
if (boostsChanged) liveSet?.boosts?.invalidateData()
|
||||
if (reportsChanged) {
|
||||
flowSet?.reports?.invalidateData()
|
||||
}
|
||||
if (zapsChanged) liveSet?.innerZaps?.invalidateData()
|
||||
if (zapsChanged) liveSet?.zaps?.invalidateData()
|
||||
|
||||
return toBeRemoved
|
||||
}
|
||||
@@ -310,7 +306,7 @@ open class Note(
|
||||
reactions = reactions + Pair(reaction, newList)
|
||||
}
|
||||
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
liveSet?.reactions?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,28 +327,28 @@ open class Note(
|
||||
if (zaps[note] != null) {
|
||||
zaps = zaps.minus(note)
|
||||
updateZapTotal()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
} else if (zaps.containsValue(note)) {
|
||||
zaps = zaps.filterValues { it != note }
|
||||
updateZapTotal()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeZapPayment(note: Note) {
|
||||
if (zapPayments.containsKey(note)) {
|
||||
zapPayments = zapPayments.minus(note)
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
} else if (zapPayments.containsValue(note)) {
|
||||
zapPayments = zapPayments.filterValues { it != note }
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun addBoost(note: Note) {
|
||||
if (note !in boosts) {
|
||||
boosts = boosts + note
|
||||
liveSet?.innerBoosts?.invalidateData()
|
||||
liveSet?.boosts?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +375,7 @@ open class Note(
|
||||
val inserted = innerAddZap(zapRequest, zap)
|
||||
if (inserted) {
|
||||
updateZapTotal()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,7 +401,7 @@ open class Note(
|
||||
if (zapPayments[zapPaymentRequest] == null) {
|
||||
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
|
||||
if (inserted) {
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,10 +413,10 @@ open class Note(
|
||||
val listOfAuthors = reactions[reaction]
|
||||
if (listOfAuthors == null) {
|
||||
reactions = reactions + Pair(reaction, listOf(note))
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
liveSet?.reactions?.invalidateData()
|
||||
} else if (!listOfAuthors.contains(note)) {
|
||||
reactions = reactions + Pair(reaction, listOfAuthors + note)
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
liveSet?.reactions?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1001,44 +997,13 @@ class NoteLiveSet(
|
||||
u: Note,
|
||||
) {
|
||||
// Observers line up here.
|
||||
val innerMetadata = NoteBundledRefresherLiveData(u)
|
||||
val innerReactions = NoteBundledRefresherLiveData(u)
|
||||
val innerBoosts = NoteBundledRefresherLiveData(u)
|
||||
val innerReplies = NoteBundledRefresherLiveData(u)
|
||||
val innerZaps = NoteBundledRefresherLiveData(u)
|
||||
val innerOts = NoteBundledRefresherLiveData(u)
|
||||
val innerModifications = NoteBundledRefresherLiveData(u)
|
||||
|
||||
val metadata = innerMetadata.map { it }
|
||||
val reactions = innerReactions.map { it }
|
||||
val boosts = innerBoosts.map { it }
|
||||
val replies = innerReplies.map { it }
|
||||
val zaps = innerZaps.map { it }
|
||||
|
||||
val hasEvent = innerMetadata.map { it.note.event != null }.distinctUntilChanged()
|
||||
|
||||
val hasReactions =
|
||||
innerZaps
|
||||
.combineWith(innerBoosts, innerReactions) { zapState, boostState, reactionState ->
|
||||
zapState?.note?.zaps?.isNotEmpty()
|
||||
?: false ||
|
||||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
|
||||
reactionState?.note?.reactions?.isNotEmpty() ?: false
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged()
|
||||
|
||||
val reactionCount =
|
||||
innerReactions
|
||||
.map {
|
||||
var total = 0
|
||||
it.note.reactions.forEach { total += it.value.size }
|
||||
total
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged()
|
||||
|
||||
val content = innerMetadata.map { it.note.event?.content ?: "" }
|
||||
val metadata = NoteBundledRefresherLiveData(u)
|
||||
val reactions = NoteBundledRefresherLiveData(u)
|
||||
val boosts = NoteBundledRefresherLiveData(u)
|
||||
val replies = NoteBundledRefresherLiveData(u)
|
||||
val zaps = NoteBundledRefresherLiveData(u)
|
||||
val ots = NoteBundledRefresherLiveData(u)
|
||||
val edits = NoteBundledRefresherLiveData(u)
|
||||
|
||||
fun isInUse(): Boolean =
|
||||
metadata.hasObservers() ||
|
||||
@@ -1046,22 +1011,17 @@ class NoteLiveSet(
|
||||
boosts.hasObservers() ||
|
||||
replies.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
hasEvent.hasObservers() ||
|
||||
hasReactions.hasObservers() ||
|
||||
replyCount.hasObservers() ||
|
||||
reactionCount.hasObservers() ||
|
||||
boostCount.hasObservers() ||
|
||||
innerOts.hasObservers() ||
|
||||
innerModifications.hasObservers()
|
||||
ots.hasObservers() ||
|
||||
edits.hasObservers()
|
||||
|
||||
fun destroy() {
|
||||
innerMetadata.destroy()
|
||||
innerReactions.destroy()
|
||||
innerBoosts.destroy()
|
||||
innerReplies.destroy()
|
||||
innerZaps.destroy()
|
||||
innerOts.destroy()
|
||||
innerModifications.destroy()
|
||||
metadata.destroy()
|
||||
reactions.destroy()
|
||||
boosts.destroy()
|
||||
replies.destroy()
|
||||
zaps.destroy()
|
||||
ots.destroy()
|
||||
edits.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,37 +1069,6 @@ class NoteBundledRefresherLiveData(
|
||||
postValue(NoteState(note))
|
||||
}
|
||||
}
|
||||
|
||||
fun <Y> map(transform: (NoteState) -> Y): NoteLoadingLiveData<Y> {
|
||||
val initialValue = this.value?.let { transform(it) }
|
||||
val result = NoteLoadingLiveData(note, initialValue)
|
||||
result.addSource(this) { x -> result.value = transform(x) }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteLoadingLiveData<Y>(
|
||||
val note: Note,
|
||||
initialValue: Y?,
|
||||
) : MediatorLiveData<Y>(initialValue) {
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
if (note is AddressableNote) {
|
||||
NostrSingleEventDataSource.addAddress(note)
|
||||
} else {
|
||||
NostrSingleEventDataSource.add(note)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
if (note is AddressableNote) {
|
||||
NostrSingleEventDataSource.removeAddress(note)
|
||||
} else {
|
||||
NostrSingleEventDataSource.remove(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable class NoteState(
|
||||
|
||||
@@ -23,9 +23,6 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
@@ -128,7 +125,7 @@ class User(
|
||||
if (event.id == latestBookmarkList?.id) return
|
||||
|
||||
latestBookmarkList = event
|
||||
liveSet?.innerBookmarks?.invalidateData()
|
||||
liveSet?.bookmarks?.invalidateData()
|
||||
}
|
||||
|
||||
fun clearEOSE() {
|
||||
@@ -142,7 +139,7 @@ class User(
|
||||
latestContactList = event
|
||||
|
||||
// Update following of the current user
|
||||
liveSet?.innerFollows?.invalidateData()
|
||||
liveSet?.follows?.invalidateData()
|
||||
flowSet?.follows?.invalidateData()
|
||||
|
||||
// Update Followers of the past user list
|
||||
@@ -151,18 +148,18 @@ class User(
|
||||
LocalCache
|
||||
.getUserIfExists(it)
|
||||
?.liveSet
|
||||
?.innerFollowers
|
||||
?.followers
|
||||
?.invalidateData()
|
||||
}
|
||||
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
|
||||
LocalCache
|
||||
.getUserIfExists(it)
|
||||
?.liveSet
|
||||
?.innerFollowers
|
||||
?.followers
|
||||
?.invalidateData()
|
||||
}
|
||||
|
||||
liveSet?.innerRelays?.invalidateData()
|
||||
liveSet?.relays?.invalidateData()
|
||||
flowSet?.relays?.invalidateData()
|
||||
}
|
||||
|
||||
@@ -172,10 +169,10 @@ class User(
|
||||
val reportsBy = reports[author]
|
||||
if (reportsBy == null) {
|
||||
reports = reports + Pair(author, setOf(note))
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
liveSet?.reports?.invalidateData()
|
||||
} else if (!reportsBy.contains(note)) {
|
||||
reports = reports + Pair(author, reportsBy + note)
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
liveSet?.reports?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +182,7 @@ class User(
|
||||
if (reports[author]?.contains(deleteNote) == true) {
|
||||
reports[author]?.let {
|
||||
reports = reports + Pair(author, it.minus(deleteNote))
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
liveSet?.reports?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,17 +193,17 @@ class User(
|
||||
) {
|
||||
if (zaps[zapRequest] == null) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeZap(zapRequestOrZapEvent: Note) {
|
||||
if (zaps.containsKey(zapRequestOrZapEvent)) {
|
||||
zaps = zaps.minus(zapRequestOrZapEvent)
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
} else if (zaps.containsValue(zapRequestOrZapEvent)) {
|
||||
zaps = zaps.filter { it.value != zapRequestOrZapEvent }
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +256,7 @@ class User(
|
||||
val privateChatroom = getOrCreatePrivateChatroom(room)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.addMessageSync(msg)
|
||||
liveSet?.innerMessages?.invalidateData()
|
||||
liveSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +267,7 @@ class User(
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.addMessageSync(msg)
|
||||
liveSet?.innerMessages?.invalidateData()
|
||||
liveSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +284,7 @@ class User(
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.removeMessageSync(msg)
|
||||
liveSet?.innerMessages?.invalidateData()
|
||||
liveSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +296,7 @@ class User(
|
||||
val privateChatroom = getOrCreatePrivateChatroom(room)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.removeMessageSync(msg)
|
||||
liveSet?.innerMessages?.invalidateData()
|
||||
liveSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +314,7 @@ class User(
|
||||
here.counter++
|
||||
}
|
||||
|
||||
liveSet?.innerRelayInfo?.invalidateData()
|
||||
liveSet?.relayInfo?.invalidateData()
|
||||
}
|
||||
|
||||
fun updateUserInfo(
|
||||
@@ -337,7 +334,7 @@ class User(
|
||||
}
|
||||
|
||||
flowSet?.metadata?.invalidateData()
|
||||
liveSet?.innerMetadata?.invalidateData()
|
||||
liveSet?.metadata?.invalidateData()
|
||||
}
|
||||
|
||||
fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
|
||||
@@ -484,36 +481,18 @@ class UserFlowSet(
|
||||
class UserLiveSet(
|
||||
u: User,
|
||||
) {
|
||||
val innerMetadata = UserBundledRefresherLiveData(u)
|
||||
val metadata = UserBundledRefresherLiveData(u)
|
||||
|
||||
// UI Observers line up here.
|
||||
val innerFollows = UserBundledRefresherLiveData(u)
|
||||
val innerFollowers = UserBundledRefresherLiveData(u)
|
||||
val innerReports = UserBundledRefresherLiveData(u)
|
||||
val innerMessages = UserBundledRefresherLiveData(u)
|
||||
val innerRelays = UserBundledRefresherLiveData(u)
|
||||
val innerRelayInfo = UserBundledRefresherLiveData(u)
|
||||
val innerZaps = UserBundledRefresherLiveData(u)
|
||||
val innerBookmarks = UserBundledRefresherLiveData(u)
|
||||
val innerStatuses = UserBundledRefresherLiveData(u)
|
||||
|
||||
// UI Observers line up here.
|
||||
val metadata = innerMetadata.map { it }
|
||||
val follows = innerFollows.map { it }
|
||||
val followers = innerFollowers.map { it }
|
||||
val reports = innerReports.map { it }
|
||||
val messages = innerMessages.map { it }
|
||||
val relays = innerRelays.map { it }
|
||||
val relayInfo = innerRelayInfo.map { it }
|
||||
val zaps = innerZaps.map { it }
|
||||
val bookmarks = innerBookmarks.map { it }
|
||||
val statuses = innerStatuses.map { it }
|
||||
|
||||
val profilePictureChanges = innerMetadata.map { it.user.profilePicture() }.distinctUntilChanged()
|
||||
|
||||
val nip05Changes = innerMetadata.map { it.user.nip05() }.distinctUntilChanged()
|
||||
|
||||
val userMetadataInfo = innerMetadata.map { it.user.info }.distinctUntilChanged()
|
||||
val follows = UserBundledRefresherLiveData(u)
|
||||
val followers = UserBundledRefresherLiveData(u)
|
||||
val reports = UserBundledRefresherLiveData(u)
|
||||
val messages = UserBundledRefresherLiveData(u)
|
||||
val relays = UserBundledRefresherLiveData(u)
|
||||
val relayInfo = UserBundledRefresherLiveData(u)
|
||||
val zaps = UserBundledRefresherLiveData(u)
|
||||
val bookmarks = UserBundledRefresherLiveData(u)
|
||||
val statuses = UserBundledRefresherLiveData(u)
|
||||
|
||||
fun isInUse(): Boolean =
|
||||
metadata.hasObservers() ||
|
||||
@@ -525,22 +504,19 @@ class UserLiveSet(
|
||||
relayInfo.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
bookmarks.hasObservers() ||
|
||||
statuses.hasObservers() ||
|
||||
profilePictureChanges.hasObservers() ||
|
||||
nip05Changes.hasObservers() ||
|
||||
userMetadataInfo.hasObservers()
|
||||
statuses.hasObservers()
|
||||
|
||||
fun destroy() {
|
||||
innerMetadata.destroy()
|
||||
innerFollows.destroy()
|
||||
innerFollowers.destroy()
|
||||
innerReports.destroy()
|
||||
innerMessages.destroy()
|
||||
innerRelays.destroy()
|
||||
innerRelayInfo.destroy()
|
||||
innerZaps.destroy()
|
||||
innerBookmarks.destroy()
|
||||
innerStatuses.destroy()
|
||||
metadata.destroy()
|
||||
follows.destroy()
|
||||
followers.destroy()
|
||||
reports.destroy()
|
||||
messages.destroy()
|
||||
relays.destroy()
|
||||
relayInfo.destroy()
|
||||
zaps.destroy()
|
||||
bookmarks.destroy()
|
||||
statuses.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,13 +546,6 @@ class UserBundledRefresherLiveData(
|
||||
postValue(UserState(user))
|
||||
}
|
||||
}
|
||||
|
||||
fun <Y> map(transform: (UserState) -> Y): UserLoadingLiveData<Y> {
|
||||
val initialValue = this.value?.let { transform(it) }
|
||||
val result = UserLoadingLiveData(user, initialValue)
|
||||
result.addSource(this) { x -> result.value = transform(x) }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
@@ -602,21 +571,6 @@ class UserBundledRefresherFlow(
|
||||
}
|
||||
}
|
||||
|
||||
class UserLoadingLiveData<Y>(
|
||||
val user: User,
|
||||
initialValue: Y?,
|
||||
) : MediatorLiveData<Y>(initialValue) {
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
NostrSingleUserDataSource.add(user)
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
NostrSingleUserDataSource.remove(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable class UserState(
|
||||
val user: User,
|
||||
)
|
||||
|
||||
@@ -1,514 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.blossom.BlossomServersEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
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.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
// TODO: Migrate this to a property of AccountVi
|
||||
object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
lateinit var account: Account
|
||||
var otherAccounts = listOf<HexKey>()
|
||||
|
||||
val latestEOSEs = EOSEAccount()
|
||||
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
|
||||
|
||||
fun createAccountMetadataFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
ContactListEvent.KIND,
|
||||
StatusEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND,
|
||||
SearchRelayListEvent.KIND,
|
||||
FileServersEvent.KIND,
|
||||
BlossomServersEvent.KIND,
|
||||
PrivateOutboxRelayListEvent.KIND,
|
||||
),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 20,
|
||||
),
|
||||
)
|
||||
|
||||
fun createOtherAccountsBaseFilter(): TypedFilter? {
|
||||
val otherAuthors = otherAccounts.filter { it != account.userProfile().pubkeyHex }
|
||||
if (otherAuthors.isEmpty()) return null
|
||||
return TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
ContactListEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND,
|
||||
SearchRelayListEvent.KIND,
|
||||
FileServersEvent.KIND,
|
||||
BlossomServersEvent.KIND,
|
||||
MuteListEvent.KIND,
|
||||
PeopleListEvent.KIND,
|
||||
),
|
||||
authors = otherAuthors,
|
||||
limit = otherAuthors.size * 20,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createAccountSettingsFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND, BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 100,
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountSettings2Filter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(AppSpecificDataEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
tags = mapOf("d" to listOf(Account.APP_SPECIFIC_DATA_D_TAG)),
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountReportsFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(DraftEvent.KIND, ReportEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultNotificationFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountLastPostsListFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 400,
|
||||
),
|
||||
)
|
||||
|
||||
fun createNotificationFilter(): TypedFilter {
|
||||
var since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultNotificationFollowList.value)
|
||||
?.relayList
|
||||
?.toMutableMap()
|
||||
|
||||
if (since == null) {
|
||||
since =
|
||||
account.connectToRelays.value
|
||||
.associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
|
||||
.toMutableMap()
|
||||
} else {
|
||||
account.connectToRelays.value.forEach {
|
||||
val eose = since.get(it.url)
|
||||
if (eose == null) {
|
||||
since.put(it.url, EOSETime(TimeUtils.oneWeekAgo()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
LnZapPaymentResponseEvent.KIND,
|
||||
ChannelMessageEvent.KIND,
|
||||
BadgeAwardEvent.KIND,
|
||||
),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
limit = 4000,
|
||||
since = since,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createNotificationFilter2(): TypedFilter {
|
||||
val since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultNotificationFollowList.value)
|
||||
?.relayList
|
||||
?: account.connectToRelays.value.associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
|
||||
?: account.convertLocalRelays().associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
|
||||
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
GitReplyEvent.KIND,
|
||||
GitIssueEvent.KIND,
|
||||
GitPatchEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
CalendarDateSlotEvent.KIND,
|
||||
CalendarTimeSlotEvent.KIND,
|
||||
CalendarRSVPEvent.KIND,
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
InteractiveStorySceneEvent.KIND,
|
||||
),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
limit = 400,
|
||||
since = since,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createGiftWrapsToMeFilter() =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(GiftWrapEvent.KIND),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get("&&((GIFTWRAPS_EOSE))&&")
|
||||
?.relayList
|
||||
?.mapValues {
|
||||
EOSETime(it.value.time - TimeUtils.twoDays())
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
val accountChannel =
|
||||
requestNewChannel { time, relayUrl ->
|
||||
if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
latestEOSEs.addOrUpdate(
|
||||
account.userProfile(),
|
||||
account.settings.defaultNotificationFollowList.value,
|
||||
relayUrl,
|
||||
time,
|
||||
)
|
||||
|
||||
latestEOSEs.addOrUpdate(
|
||||
account.userProfile(),
|
||||
"&&((GIFTWRAPS_EOSE))&&",
|
||||
relayUrl,
|
||||
time,
|
||||
)
|
||||
} else {
|
||||
hasLoadedTheBasics[account.userProfile()] = true
|
||||
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
override fun consume(
|
||||
event: Event,
|
||||
relay: Relay,
|
||||
) {
|
||||
if (LocalCache.justVerify(event)) {
|
||||
consumeAlreadyVerified(event, relay)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeAlreadyVerified(
|
||||
event: Event,
|
||||
relay: Relay,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
when (event) {
|
||||
is OtsEvent -> {
|
||||
// verifies new OTS upon arrival
|
||||
Amethyst.instance.otsVerifCache.cacheVerify(event, account::otsResolver)
|
||||
}
|
||||
|
||||
is PrivateOutboxRelayListEvent -> {
|
||||
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
|
||||
val noteEvent = note?.event
|
||||
if (noteEvent == null || event.createdAt > noteEvent.createdAt) {
|
||||
event.privateTags(account.signer) {
|
||||
LocalCache.justConsume(event, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DraftEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
|
||||
if (!event.isDeleted()) {
|
||||
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
|
||||
val noteEvent = note?.event
|
||||
if (noteEvent != null) {
|
||||
if (event.createdAt > noteEvent.createdAt || relay.brief !in note.relays) {
|
||||
LocalCache.consume(event, relay)
|
||||
}
|
||||
} else {
|
||||
// decrypts
|
||||
event.cachedDraft(account.signer) {}
|
||||
|
||||
LocalCache.justConsume(event, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is GiftWrapEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
val noteEvent = note?.event as? GiftWrapEvent
|
||||
if (noteEvent != null) {
|
||||
if (relay.brief !in note.relays) {
|
||||
LocalCache.justConsume(noteEvent, relay)
|
||||
|
||||
noteEvent.innerEventId?.let {
|
||||
(LocalCache.getNoteIfExists(it)?.event as? Event)?.let {
|
||||
this.consumeAlreadyVerified(it, relay)
|
||||
}
|
||||
} ?: run {
|
||||
event.unwrap(account.signer) {
|
||||
this.consume(it, relay)
|
||||
noteEvent.innerEventId = it.id
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// new event
|
||||
event.unwrap(account.signer) {
|
||||
LocalCache.justConsume(event, relay)
|
||||
this.consume(it, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SealedRumorEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
val noteEvent = note?.event as? SealedRumorEvent
|
||||
if (noteEvent != null) {
|
||||
if (relay.brief !in note.relays) {
|
||||
LocalCache.justConsume(noteEvent, relay)
|
||||
|
||||
noteEvent.innerEventId?.let {
|
||||
(LocalCache.getNoteIfExists(it)?.event as? Event)?.let {
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
} ?: run {
|
||||
event.unseal(account.signer) {
|
||||
LocalCache.justConsume(it, relay)
|
||||
noteEvent.innerEventId = it.id
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// new event
|
||||
event.unseal(account.signer) {
|
||||
LocalCache.justConsume(event, relay)
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is LnZapEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note?.event == null) {
|
||||
event.zapRequest?.let {
|
||||
if (it.isPrivateZap()) {
|
||||
it.decryptPrivateZap(account.signer) {}
|
||||
}
|
||||
}
|
||||
|
||||
LocalCache.justConsume(event, relay)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
LocalCache.justConsume(event, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markAsSeenOnRelay(
|
||||
eventId: String,
|
||||
relay: Relay,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
super.markAsSeenOnRelay(eventId, relay)
|
||||
|
||||
val note = LocalCache.getNoteIfExists(eventId) ?: return
|
||||
val noteEvent = note.event ?: return
|
||||
markInnerAsSeenOnRelay(noteEvent, relay)
|
||||
}
|
||||
|
||||
private fun markInnerAsSeenOnRelay(
|
||||
newNoteEvent: Event,
|
||||
relay: Relay,
|
||||
) {
|
||||
markInnerAsSeenOnRelay(newNoteEvent.id, relay)
|
||||
}
|
||||
|
||||
private fun markInnerAsSeenOnRelay(
|
||||
eventId: HexKey,
|
||||
relay: Relay,
|
||||
) {
|
||||
val note = LocalCache.getNoteIfExists(eventId)
|
||||
|
||||
if (note != null) {
|
||||
note.addRelay(relay)
|
||||
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is GiftWrapEvent) {
|
||||
noteEvent.innerEventId?.let {
|
||||
markInnerAsSeenOnRelay(it, relay)
|
||||
}
|
||||
} else if (noteEvent is SealedRumorEvent) {
|
||||
noteEvent.innerEventId?.let {
|
||||
markInnerAsSeenOnRelay(it, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() =
|
||||
if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
// gets everything about the user logged in
|
||||
accountChannel.typedFilters =
|
||||
listOfNotNull(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountSettings2Filter(),
|
||||
createNotificationFilter(),
|
||||
createNotificationFilter2(),
|
||||
createGiftWrapsToMeFilter(),
|
||||
createAccountReportsFilter(),
|
||||
createAccountSettingsFilter(),
|
||||
createAccountLastPostsListFilter(),
|
||||
createOtherAccountsBaseFilter(),
|
||||
).ifEmpty { null }
|
||||
} else {
|
||||
// just the basics.
|
||||
accountChannel.typedFilters =
|
||||
listOf(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountSettingsFilter(),
|
||||
createAccountSettings2Filter(),
|
||||
).ifEmpty { null }
|
||||
}
|
||||
|
||||
override fun auth(
|
||||
relay: Relay,
|
||||
challenge: String,
|
||||
) {
|
||||
super.auth(relay, challenge)
|
||||
|
||||
if (this::account.isInitialized) {
|
||||
account.sendAuthEvent(relay, challenge)
|
||||
}
|
||||
}
|
||||
|
||||
override fun notify(
|
||||
relay: Relay,
|
||||
description: String,
|
||||
) {
|
||||
super.notify(relay, description)
|
||||
|
||||
if (this::account.isInitialized) {
|
||||
account.addPaymentRequestIfNew(Account.PaymentRequest(relay.url, description))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
|
||||
object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
var account: Account? = null
|
||||
var channel: Channel? = null
|
||||
|
||||
fun loadMessagesBetween(
|
||||
account: Account,
|
||||
channel: Channel,
|
||||
) {
|
||||
this.account = account
|
||||
this.channel = channel
|
||||
resetFilters()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
account = null
|
||||
channel = null
|
||||
}
|
||||
|
||||
fun createMessagesByMeToChannelFilter(): TypedFilter? {
|
||||
val myAccount = account ?: return null
|
||||
|
||||
if (channel is PublicChatChannel) {
|
||||
// Brings on messages by the user from all other relays.
|
||||
// Since we ship with write to public, read from private only
|
||||
// this guarantees that messages from the author do not disappear.
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
authors = listOf(myAccount.userProfile().pubkeyHex),
|
||||
limit = 50,
|
||||
),
|
||||
)
|
||||
} else if (channel is LiveActivitiesChannel) {
|
||||
// Brings on messages by the user from all other relays.
|
||||
// Since we ship with write to public, read from private only
|
||||
// this guarantees that messages from the author do not disappear.
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
|
||||
authors = listOf(myAccount.userProfile().pubkeyHex),
|
||||
limit = 50,
|
||||
),
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun createMessagesToChannelFilter(): TypedFilter? {
|
||||
if (channel is PublicChatChannel) {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
tags = mapOf("e" to listOfNotNull(channel?.idHex)),
|
||||
limit = 200,
|
||||
),
|
||||
)
|
||||
} else if (channel is LiveActivitiesChannel) {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
|
||||
tags = mapOf("a" to listOfNotNull(channel?.idHex)),
|
||||
limit = 200,
|
||||
),
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val messagesChannel = requestNewChannel()
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
messagesChannel.typedFilters =
|
||||
listOfNotNull(
|
||||
createMessagesToChannelFilter(),
|
||||
createMessagesByMeToChannelFilter(),
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
|
||||
object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
lateinit var account: Account
|
||||
private var withRoom: ChatroomKey? = null
|
||||
|
||||
private val latestEOSEs = EOSEAccount()
|
||||
|
||||
fun loadMessagesBetween(
|
||||
accountIn: Account,
|
||||
withRoom: ChatroomKey,
|
||||
) {
|
||||
this.account = accountIn
|
||||
this.withRoom = withRoom
|
||||
resetFilters()
|
||||
}
|
||||
|
||||
fun createMessagesToMeFilter(): TypedFilter? {
|
||||
val myPeer = withRoom
|
||||
|
||||
return if (myPeer != null) {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
authors = myPeer.users.toList(),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(withRoom.hashCode().toString())
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun createMessagesFromMeFilter(): TypedFilter? {
|
||||
val myPeer = withRoom
|
||||
|
||||
return if (myPeer != null) {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
tags = mapOf("p" to myPeer.users.map { it }),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(withRoom.hashCode().toString())
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun clearEOSEs(account: Account) {
|
||||
latestEOSEs.removeDataFor(account.userProfile())
|
||||
}
|
||||
|
||||
val inandoutChannel =
|
||||
requestNewChannel { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), withRoom.hashCode().toString(), relayUrl, time)
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
inandoutChannel.typedFilters =
|
||||
listOfNotNull(
|
||||
createMessagesToMeFilter(),
|
||||
createMessagesFromMeFilter(),
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
|
||||
object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed") {
|
||||
private var communityToWatch: AddressableNote? = null
|
||||
|
||||
private fun createLoadCommunityFilter(): TypedFilter? {
|
||||
val myCommunityToWatch = communityToWatch ?: return null
|
||||
|
||||
val community = myCommunityToWatch.event as? CommunityDefinitionEvent ?: return null
|
||||
|
||||
val authors =
|
||||
community
|
||||
.moderators()
|
||||
.map { it.pubKey }
|
||||
.plus(listOfNotNull(myCommunityToWatch.author?.pubkeyHex))
|
||||
|
||||
if (authors.isEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
authors = authors,
|
||||
tags =
|
||||
mapOf(
|
||||
"a" to listOf(myCommunityToWatch.address.toValue()),
|
||||
),
|
||||
kinds = listOf(CommunityPostApprovalEvent.KIND),
|
||||
limit = 500,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val loadCommunityChannel = requestNewChannel()
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
loadCommunityChannel.typedFilters = listOfNotNull(createLoadCommunityFilter()).ifEmpty { null }
|
||||
}
|
||||
|
||||
fun loadCommunity(note: AddressableNote?) {
|
||||
communityToWatch = note
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
-455
@@ -1,455 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val scope = Amethyst.instance.applicationIOScope
|
||||
val latestEOSEs = EOSEAccount()
|
||||
|
||||
var job: Job? = null
|
||||
|
||||
override fun start() {
|
||||
job?.cancel()
|
||||
job =
|
||||
scope.launch(Dispatchers.IO) {
|
||||
account.liveDiscoveryFollowLists.collect {
|
||||
if (this@NostrDiscoveryDataSource::account.isInitialized) {
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
super.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
fun createMarketplaceFilter(): List<TypedFilter> {
|
||||
val follows = account.liveDiscoveryListAuthorsPerRelay.value?.ifEmpty { null }
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.hashtags
|
||||
?.toList()
|
||||
?.ifEmpty { null }
|
||||
val geohashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.geotags
|
||||
?.toList()
|
||||
?.ifEmpty { null }
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
SinceAuthorPerRelayFilter(
|
||||
authors = follows,
|
||||
kinds = listOf(ClassifiedsEvent.KIND),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
),
|
||||
hashToLoad?.let {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ClassifiedsEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
"t" to
|
||||
it
|
||||
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
|
||||
.flatten(),
|
||||
),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
},
|
||||
geohashToLoad?.let {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ClassifiedsEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
"g" to it,
|
||||
),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun createNIP89Filter(kTags: List<String>): List<TypedFilter> =
|
||||
listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(AppDefinitionEvent.KIND),
|
||||
limit = 300,
|
||||
tags = mapOf("k" to kTags),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
fun createLiveStreamFilter(): List<TypedFilter> {
|
||||
val follows =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.authors
|
||||
?.toList()
|
||||
?.ifEmpty { null }
|
||||
|
||||
val followsRelays = account.liveDiscoveryListAuthorsPerRelay.value
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
SinceAuthorPerRelayFilter(
|
||||
authors = followsRelays,
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
),
|
||||
follows?.let {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
tags = mapOf("p" to it),
|
||||
kinds = listOf(LiveActivitiesEvent.KIND),
|
||||
limit = 100,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun createPublicChatFilter(): List<TypedFilter> {
|
||||
val follows = account.liveDiscoveryListAuthorsPerRelay.value?.ifEmpty { null }
|
||||
val followChats = account.selectedChatsFollowList().toList()
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
SinceAuthorPerRelayFilter(
|
||||
authors = follows,
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
limit = 500,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
),
|
||||
if (followChats.isNotEmpty()) {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
ids = followChats,
|
||||
kinds = listOf(ChannelCreateEvent.KIND, ChannelMessageEvent.KIND),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun createCommunitiesFilter(): TypedFilter {
|
||||
val follows = account.liveDiscoveryListAuthorsPerRelay.value
|
||||
|
||||
return TypedFilter(
|
||||
types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
SinceAuthorPerRelayFilter(
|
||||
authors = follows,
|
||||
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createLiveStreamTagsFilter(): TypedFilter? {
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.hashtags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
"t" to
|
||||
hashToLoad
|
||||
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
|
||||
.flatten(),
|
||||
),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createLiveStreamGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.geotags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
|
||||
tags = mapOf("g" to hashToLoad),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createPublicChatsTagsFilter(): TypedFilter? {
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.hashtags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
"t" to
|
||||
hashToLoad
|
||||
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
|
||||
.flatten(),
|
||||
),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createPublicChatsGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.geotags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
|
||||
tags =
|
||||
mapOf("g" to hashToLoad),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createCommunitiesTagsFilter(): TypedFilter? {
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.hashtags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
"t" to
|
||||
hashToLoad
|
||||
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
|
||||
.flatten(),
|
||||
),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createCommunitiesGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.geotags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
|
||||
tags = mapOf("g" to hashToLoad),
|
||||
limit = 300,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val discoveryFeedChannel =
|
||||
requestNewChannel { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(
|
||||
account.userProfile(),
|
||||
account.settings.defaultDiscoveryFollowList.value,
|
||||
relayUrl,
|
||||
time,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
discoveryFeedChannel.typedFilters =
|
||||
createLiveStreamFilter()
|
||||
.plus(createNIP89Filter(listOf("5300")))
|
||||
.plus(createPublicChatFilter())
|
||||
.plus(createMarketplaceFilter())
|
||||
.plus(
|
||||
listOfNotNull(
|
||||
createLiveStreamTagsFilter(),
|
||||
createLiveStreamGeohashesFilter(),
|
||||
createCommunitiesFilter(),
|
||||
createCommunitiesTagsFilter(),
|
||||
createCommunitiesGeohashesFilter(),
|
||||
createPublicChatsTagsFilter(),
|
||||
createPublicChatsGeohashesFilter(),
|
||||
),
|
||||
).toList()
|
||||
.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
-232
@@ -1,232 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
|
||||
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFeed") {
|
||||
private var searchString: String? = null
|
||||
|
||||
private fun createAnythingWithIDFilter(): List<TypedFilter>? {
|
||||
val mySearchString = searchString
|
||||
if (mySearchString.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val hexToWatch =
|
||||
try {
|
||||
val isAStraightHex =
|
||||
if (Hex.isHex(mySearchString)) {
|
||||
Hex.decode(mySearchString).toHexKey()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
when (val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity) {
|
||||
is NSec -> Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey()
|
||||
is NPub -> parsed.hex
|
||||
is NProfile -> parsed.hex
|
||||
is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> parsed.hex
|
||||
is NEvent -> parsed.hex
|
||||
is NEmbed -> parsed.event.id
|
||||
is NRelay -> null
|
||||
is NAddress -> parsed.aTag()
|
||||
else -> isAStraightHex
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
val directReferenceFilters =
|
||||
hexToWatch?.let {
|
||||
if (it.contains(":")) {
|
||||
// naddr
|
||||
listOfNotNull(
|
||||
ATag.parse(it, null)?.let { aTag ->
|
||||
TypedFilter(
|
||||
types = ALL_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND, aTag.kind),
|
||||
authors = listOfNotNull(aTag.pubKeyHex),
|
||||
// just to be sure
|
||||
limit = 5,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// event ids
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = ALL_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
ids = listOfNotNull(it),
|
||||
),
|
||||
),
|
||||
// authors
|
||||
TypedFilter(
|
||||
types = ALL_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOfNotNull(it),
|
||||
// just to be sure
|
||||
limit = 5,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
// downloads all the reactions to a given event.
|
||||
return directReferenceFilters +
|
||||
listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
search = mySearchString,
|
||||
limit = 1000,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
BadgeDefinitionEvent.KIND,
|
||||
PeopleListEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
AudioTrackEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
ChannelCreateEvent.KIND,
|
||||
),
|
||||
search = mySearchString,
|
||||
limit = 100,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
ChannelMetadataEvent.KIND,
|
||||
ClassifiedsEvent.KIND,
|
||||
CommunityDefinitionEvent.KIND,
|
||||
EmojiPackEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
LiveActivitiesEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
NNSEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
),
|
||||
search = mySearchString,
|
||||
limit = 100,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
InteractiveStorySceneEvent.KIND,
|
||||
),
|
||||
search = mySearchString,
|
||||
limit = 100,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val searchChannel = requestNewChannel()
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
searchChannel.typedFilters = createAnythingWithIDFilter()
|
||||
}
|
||||
|
||||
fun search(searchString: String) {
|
||||
if (this.searchString != searchString) {
|
||||
println("DataSource: ${this.javaClass.simpleName} Search for $searchString")
|
||||
this.searchString = searchString
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
if (searchString != null) {
|
||||
println("DataSource: ${this.javaClass.simpleName} Clear")
|
||||
searchString = null
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
|
||||
object NostrThreadDataSource : AmethystNostrDataSource("SingleThreadFeed") {
|
||||
private var eventToWatch: String? = null
|
||||
|
||||
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter> {
|
||||
val threadToLoad = eventToWatch ?: return emptyList()
|
||||
|
||||
val branch = ThreadAssembler().findThreadFor(threadToLoad) ?: return emptyList()
|
||||
|
||||
val eventsToLoad =
|
||||
branch.allNotes
|
||||
.filter { it.event == null }
|
||||
.map { it.idHex }
|
||||
.toSet()
|
||||
.ifEmpty { null }
|
||||
|
||||
val address = if (branch.root is AddressableNote) branch.root.idHex else null
|
||||
val event = if (branch.root !is AddressableNote) branch.root.idHex else branch.root.event?.id
|
||||
|
||||
return listOfNotNull(
|
||||
eventsToLoad?.let {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
ids = it.toList(),
|
||||
),
|
||||
)
|
||||
},
|
||||
event?.let {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
tags =
|
||||
mapOf(
|
||||
"e" to listOf(event),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
address?.let {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
tags =
|
||||
mapOf(
|
||||
"a" to listOf(address),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val loadEventsChannel =
|
||||
requestNewChannel { _, _ ->
|
||||
// Many relays operate with limits in the amount of filters.
|
||||
// As information comes, the filters will be rotated to get more data.
|
||||
invalidateFilters()
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
loadEventsChannel.typedFilters = createLoadEventsIfNotLoadedFilter()
|
||||
}
|
||||
|
||||
fun loadThread(noteId: String?) {
|
||||
if (eventToWatch != noteId) {
|
||||
eventToWatch = noteId
|
||||
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
@@ -374,7 +373,7 @@ class ZapPaymentHandler(
|
||||
stringRes(
|
||||
context,
|
||||
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
user?.toBestDisplayName() ?: splitSetup.mainId(),
|
||||
toUser?.toBestDisplayName() ?: splitSetup.mainId(),
|
||||
),
|
||||
null,
|
||||
)
|
||||
|
||||
-4
@@ -25,7 +25,6 @@ import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
@@ -49,7 +48,6 @@ class ConnectivityFlow(
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
checkNotInMainThread()
|
||||
Log.d("ConnectivityFlow", "onAvailable ${network.networkHandle}")
|
||||
connectivityManager.getNetworkCapabilities(network)?.let {
|
||||
trySend(ConnectivityStatus.Active(network.networkHandle, it.isMeteredOrMobileData()))
|
||||
@@ -61,7 +59,6 @@ class ConnectivityFlow(
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||
checkNotInMainThread()
|
||||
val isMobile = networkCapabilities.isMeteredOrMobileData()
|
||||
Log.d("ConnectivityFlow", "onCapabilitiesChanged ${network.networkHandle} $isMobile")
|
||||
trySend(ConnectivityStatus.Active(network.networkHandle, isMobile))
|
||||
@@ -77,7 +74,6 @@ class ConnectivityFlow(
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
checkNotInMainThread()
|
||||
Log.d("ConnectivityFlow", "Stopping Connectivity Flow")
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
trySend(ConnectivityStatus.Off)
|
||||
|
||||
+23
-17
@@ -21,38 +21,44 @@
|
||||
package com.vitorpamplona.amethyst.service.eventCache
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class MemoryTrimmingService {
|
||||
class MemoryTrimmingService(
|
||||
val cache: LocalCache,
|
||||
) {
|
||||
var isTrimmingMemoryMutex = AtomicBoolean(false)
|
||||
|
||||
private suspend fun doTrim(account: Account?) {
|
||||
LocalCache.cleanObservers()
|
||||
|
||||
val accounts = LocalPreferences.allSavedAccounts().mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet()
|
||||
private suspend fun doTrim(
|
||||
account: Account? = null,
|
||||
otherAccounts: List<AccountInfo>,
|
||||
) {
|
||||
cache.cleanObservers()
|
||||
|
||||
account?.let {
|
||||
LocalCache.pruneHiddenMessages(it)
|
||||
LocalCache.pruneOldAndHiddenMessages(it)
|
||||
NostrChatroomDataSource.clearEOSEs(it)
|
||||
|
||||
LocalCache.pruneContactLists(accounts)
|
||||
LocalCache.pruneRepliesAndReactions(accounts)
|
||||
LocalCache.prunePastVersionsOfReplaceables()
|
||||
LocalCache.pruneExpiredEvents()
|
||||
cache.pruneHiddenEvents(it)
|
||||
cache.pruneHiddenMessages(it)
|
||||
}
|
||||
|
||||
val accounts = otherAccounts.mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet()
|
||||
cache.pruneOldMessages()
|
||||
cache.pruneContactLists(accounts)
|
||||
cache.pruneRepliesAndReactions(accounts)
|
||||
cache.prunePastVersionsOfReplaceables()
|
||||
cache.pruneExpiredEvents()
|
||||
}
|
||||
|
||||
suspend fun run(account: Account?) {
|
||||
suspend fun run(
|
||||
account: Account?,
|
||||
otherAccounts: List<AccountInfo>,
|
||||
) {
|
||||
if (isTrimmingMemoryMutex.compareAndSet(false, true)) {
|
||||
Log.d("ServiceManager", "Trimming Memory")
|
||||
try {
|
||||
doTrim(account)
|
||||
doTrim(account, otherAccounts)
|
||||
} finally {
|
||||
isTrimmingMemoryMutex.getAndSet(false)
|
||||
}
|
||||
|
||||
+1
-1
@@ -32,6 +32,7 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.size.Precision
|
||||
import coil3.svg.SvgDecoder
|
||||
import coil3.util.DebugLogger
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import okhttp3.Call
|
||||
|
||||
class ImageLoaderSetup {
|
||||
@@ -40,7 +41,6 @@ class ImageLoaderSetup {
|
||||
app: Application,
|
||||
diskCache: DiskCache,
|
||||
memoryCache: MemoryCache,
|
||||
isDebug: Boolean,
|
||||
callFactory: () -> Call.Factory,
|
||||
) {
|
||||
SingletonImageLoader.setUnsafe(
|
||||
|
||||
-20
@@ -24,26 +24,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerSta
|
||||
import com.vitorpamplona.amethyst.service.playback.service.PlaybackServiceClient
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
* Copyright (c) 2024 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.
|
||||
*/
|
||||
object BackgroundMedia {
|
||||
// background playing mutex.
|
||||
val bgInstance = MutableStateFlow<MediaControllerState?>(null)
|
||||
|
||||
-20
@@ -29,26 +29,6 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
|
||||
/**
|
||||
* Copyright (c) 2024 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.
|
||||
*/
|
||||
class SimultaneousPlaybackCalculator {
|
||||
companion object {
|
||||
fun isLowMemory(context: Context): Boolean {
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.datasources.EventCollector
|
||||
import com.vitorpamplona.ammolite.relays.datasources.RelayInsertConfirmationCollector
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
|
||||
class CacheClientConnector(
|
||||
val client: NostrClient,
|
||||
val cache: LocalCache,
|
||||
) {
|
||||
val receiver =
|
||||
EventCollector(client) { event, relay ->
|
||||
cache.verifyAndConsume(event, relay)
|
||||
}
|
||||
|
||||
val confirmationWatcher =
|
||||
RelayInsertConfirmationCollector(client) { eventId, relay ->
|
||||
cache.markAsSeen(eventId, relay)
|
||||
unwrapAndMarkAsSeen(eventId, relay)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
receiver.destroy()
|
||||
confirmationWatcher.destroy()
|
||||
}
|
||||
|
||||
private fun unwrapAndMarkAsSeen(
|
||||
eventId: HexKey,
|
||||
relay: Relay,
|
||||
) {
|
||||
val note = LocalCache.getNoteIfExists(eventId)
|
||||
|
||||
if (note != null) {
|
||||
note.addRelay(relay)
|
||||
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is GiftWrapEvent) {
|
||||
noteEvent.innerEventId?.let {
|
||||
unwrapAndMarkAsSeen(it, relay)
|
||||
}
|
||||
} else if (noteEvent is SealedRumorEvent) {
|
||||
noteEvent.innerEventId?.let {
|
||||
unwrapAndMarkAsSeen(it, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.MutableQueryBasedSubscriptionOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.MutableQueryState
|
||||
|
||||
@Composable
|
||||
fun <T> KeyDataSourceSubscription(
|
||||
state: T,
|
||||
dataSource: QueryBasedSubscriptionOrchestrator<T>,
|
||||
) {
|
||||
DisposableEffect(state) {
|
||||
dataSource.subscribe(state)
|
||||
onDispose {
|
||||
dataSource.unsubscribe(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T : MutableQueryState> KeyDataSourceSubscription(
|
||||
state: T,
|
||||
dataSource: MutableQueryBasedSubscriptionOrchestrator<T>,
|
||||
) {
|
||||
DisposableEffect(state) {
|
||||
dataSource.subscribe(state)
|
||||
onDispose {
|
||||
dataSource.unsubscribe(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-17
@@ -18,29 +18,35 @@
|
||||
* 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.chats.rooms
|
||||
package com.vitorpamplona.amethyst.service.relayClient.authCommand.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.ScreenAuthAccount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun WatchLifecycleAndRefreshDataSource(accountViewModel: AccountViewModel) {
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifeCycleOwner) {
|
||||
val observer =
|
||||
LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
NostrChatroomListDataSource.account = accountViewModel.account
|
||||
NostrChatroomListDataSource.start()
|
||||
}
|
||||
}
|
||||
fun RelayAuthSubscription(accountViewModel: AccountViewModel) = RelayAuthSubscription(accountViewModel, Amethyst.instance.authCoordinator)
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
|
||||
@Composable
|
||||
fun RelayAuthSubscription(
|
||||
accountViewModel: AccountViewModel,
|
||||
dataSource: AuthCoordinator,
|
||||
) {
|
||||
// different screens get different states
|
||||
// even if they are tracking the same tag.
|
||||
val state =
|
||||
remember(accountViewModel) {
|
||||
ScreenAuthAccount(accountViewModel.account)
|
||||
}
|
||||
|
||||
DisposableEffect(state) {
|
||||
dataSource.subscribe(state)
|
||||
onDispose {
|
||||
dataSource.unsubscribe(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.authCommand.model
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.datasources.RelayAuthenticator
|
||||
import kotlin.collections.forEach
|
||||
|
||||
class ScreenAuthAccount(
|
||||
val account: Account,
|
||||
)
|
||||
|
||||
class AuthCoordinator(
|
||||
client: NostrClient,
|
||||
) {
|
||||
private val authWithAccounts = ListWithUniqueSetCache<ScreenAuthAccount, Account> { it.account }
|
||||
|
||||
val receiver =
|
||||
RelayAuthenticator(client) { challenge, relay ->
|
||||
authWithAccounts.distinct().forEach {
|
||||
it.sendAuthEvent(relay, challenge)
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
receiver.destroy()
|
||||
}
|
||||
|
||||
// This is called by main. Keep it really fast.
|
||||
fun subscribe(account: ScreenAuthAccount?) {
|
||||
if (account == null) return
|
||||
|
||||
if (isDebug) {
|
||||
Log.d(this::class.simpleName, "Watch $account")
|
||||
}
|
||||
|
||||
authWithAccounts.add(account)
|
||||
}
|
||||
|
||||
// This is called by main. Keep it really fast.
|
||||
fun unsubscribe(account: ScreenAuthAccount?) {
|
||||
if (account == null) return
|
||||
|
||||
if (isDebug) {
|
||||
Log.d(this::class.simpleName, "Unwatch $account")
|
||||
}
|
||||
|
||||
authWithAccounts.remove(account)
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.authCommand.model
|
||||
|
||||
import androidx.lifecycle.AtomicReference
|
||||
|
||||
class ListWithUniqueSetCache<T, U>(
|
||||
val key: (T) -> U,
|
||||
) {
|
||||
private val list = AtomicReference(listOf<T>())
|
||||
private val cacheSet = AtomicReference<Set<U>?>(setOf<U>())
|
||||
|
||||
fun isEmpty() = list.get().isEmpty()
|
||||
|
||||
fun add(item: T) = set(list.get() + item)
|
||||
|
||||
fun remove(item: T) = set(list.get() - item)
|
||||
|
||||
fun set(newList: List<T>) {
|
||||
list.set(newList)
|
||||
// Invalidate the cache - next read will recompute
|
||||
cacheSet.set(null)
|
||||
}
|
||||
|
||||
fun distinct(): Set<U> {
|
||||
var currentSet = cacheSet.get()
|
||||
|
||||
// Check if the cached set is based on the current list
|
||||
if (currentSet != null) {
|
||||
return currentSet
|
||||
}
|
||||
|
||||
// Compute and attempt to atomically update the cache
|
||||
val newSet = list.get().mapTo(mutableSetOf<U>(), key)
|
||||
cacheSet.compareAndSet(currentSet, newSet)
|
||||
return newSet
|
||||
}
|
||||
|
||||
fun forEachSubscriber(action: (T) -> Unit) {
|
||||
list.get().forEach(action)
|
||||
}
|
||||
|
||||
fun forEachUniqueSubscriber(action: (U) -> Unit) {
|
||||
distinct().forEach(action)
|
||||
}
|
||||
}
|
||||
+15
-9
@@ -18,12 +18,13 @@
|
||||
* 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.components
|
||||
package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NotifyRequestDialog
|
||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyRequestsCache
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -33,11 +34,17 @@ import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
||||
fun DisplayNotifyMessages(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val openDialogMsg =
|
||||
accountViewModel.account.transientPaymentRequests.collectAsStateWithLifecycle(null)
|
||||
) = DisplayNotifyMessages(Amethyst.instance.notifyCoordinator.requests, accountViewModel, nav)
|
||||
|
||||
openDialogMsg.value?.firstOrNull()?.let { request ->
|
||||
@Composable
|
||||
fun DisplayNotifyMessages(
|
||||
requests: NotifyRequestsCache,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val openDialogMsg = requests.transientPaymentRequests.collectAsStateWithLifecycle(emptySet())
|
||||
|
||||
openDialogMsg.value.firstOrNull()?.let { request ->
|
||||
NotifyRequestDialog(
|
||||
title =
|
||||
stringRes(
|
||||
@@ -47,8 +54,7 @@ fun DisplayNotifyMessages(
|
||||
textContent = request.description,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
) {
|
||||
accountViewModel.dismissPaymentRequest(request)
|
||||
}
|
||||
onDismiss = { requests.dismissPaymentRequest(request) },
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.actions
|
||||
package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.notifyCommand.model
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.datasources.RelayNotifier
|
||||
|
||||
class NotifyCoordinator(
|
||||
client: NostrClient,
|
||||
) {
|
||||
val requests = NotifyRequestsCache()
|
||||
|
||||
val receiver =
|
||||
RelayNotifier(client) { message, relay ->
|
||||
requests.addPaymentRequestIfNew(message, relay.url)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
receiver.destroy()
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.notifyCommand.model
|
||||
|
||||
data class NotifyRequest(
|
||||
val relayUrl: String,
|
||||
val description: String,
|
||||
)
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.notifyCommand.model
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class NotifyRequestsCache {
|
||||
val transientPaymentRequestDismissals: MutableStateFlow<Set<NotifyRequest>> = MutableStateFlow(emptySet())
|
||||
val transientPaymentRequests: MutableStateFlow<Set<NotifyRequest>> = MutableStateFlow(emptySet())
|
||||
|
||||
fun addPaymentRequestIfNew(
|
||||
description: String,
|
||||
relayUrl: String,
|
||||
) {
|
||||
addPaymentRequestIfNew(NotifyRequest(description, relayUrl))
|
||||
}
|
||||
|
||||
fun addPaymentRequestIfNew(paymentRequest: NotifyRequest) {
|
||||
if (
|
||||
!this.transientPaymentRequests.value.contains(paymentRequest) &&
|
||||
!this.transientPaymentRequestDismissals.value.contains(paymentRequest)
|
||||
) {
|
||||
this.transientPaymentRequests.value += paymentRequest
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissPaymentRequest(request: NotifyRequest) {
|
||||
if (this.transientPaymentRequests.value.contains(request)) {
|
||||
this.transientPaymentRequests.update { it - request }
|
||||
this.transientPaymentRequestDismissals.update { it + request }
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.datasources.SubscriptionOrchestrator
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Creates a data source where the filters are derived from
|
||||
* data (keys) that is being watched in the UI.
|
||||
*/
|
||||
abstract class QueryBasedSubscriptionOrchestrator<T>(
|
||||
client: NostrClient,
|
||||
) : SubscriptionOrchestrator(client) {
|
||||
private var queries: ConcurrentHashMap<T, T> = ConcurrentHashMap()
|
||||
|
||||
// This is called by main. Keep it really fast.
|
||||
fun subscribe(query: T?) {
|
||||
if (query == null) return
|
||||
|
||||
val wasEmpty = queries.isEmpty()
|
||||
|
||||
queries.put(query, query)
|
||||
|
||||
if (wasEmpty) {
|
||||
start()
|
||||
}
|
||||
|
||||
invalidateFilters()
|
||||
|
||||
if (isDebug) {
|
||||
Log.d(this::class.simpleName, "Watch $query (${queries.size} queries)")
|
||||
}
|
||||
}
|
||||
|
||||
// This is called by main. Keep it really fast.
|
||||
fun unsubscribe(query: T?) {
|
||||
if (query == null) return
|
||||
|
||||
queries.remove(query)
|
||||
|
||||
invalidateFilters()
|
||||
|
||||
if (queries.isEmpty()) {
|
||||
stop()
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
Log.d(this::class.simpleName, "Unwatch $query (${queries.size} queries)")
|
||||
}
|
||||
}
|
||||
|
||||
fun forEachSubscriber(action: (T) -> Unit) {
|
||||
queries.keys.forEach(action)
|
||||
}
|
||||
|
||||
final override fun updateSubscriptions() {
|
||||
updateSubscriptions(queries.keys)
|
||||
}
|
||||
|
||||
abstract fun updateSubscriptions(keys: Set<T>)
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource.ChatroomFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class RelaySubscriptionsCoordinator(
|
||||
cache: LocalCache,
|
||||
client: NostrClient,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
// main one: notifications, dms and account settings
|
||||
val account = AccountFilterAssembler(client)
|
||||
|
||||
// always running, feed assemblers.
|
||||
val home = HomeFilterAssembler(client)
|
||||
val chatroomList = ChatroomListFilterAssembler(client)
|
||||
val video = VideoFilterAssembler(client)
|
||||
val discovery = DiscoveryFilterAssembler(client)
|
||||
|
||||
// loaders of content that is not yet in the device.
|
||||
// they are active when looking at events, users, channels.
|
||||
val channelFinder = ChannelFinderFilterAssembler(client)
|
||||
val eventFinder = EventFinderFilterAssembler(client)
|
||||
val userFinder = UserFinderFilterAssembler(client)
|
||||
|
||||
// active when searching or tagging users.
|
||||
val search = SearchFilterAssembler(cache, client, scope)
|
||||
|
||||
// active depending on the screen.
|
||||
val channel = ChannelFilterAssembler(client)
|
||||
val chatroom = ChatroomFilterAssembler(client)
|
||||
val community = CommunityFilterAssembler(client)
|
||||
val thread = ThreadFilterAssembler(client)
|
||||
val profile = UserProfileFilterAssembler(client)
|
||||
val hashtags = HashtagFilterAssembler(client)
|
||||
val geohashes = GeoHashFilterAssembler(client)
|
||||
|
||||
// active when sending zaps via NWC
|
||||
val nwc = NWCPaymentFilterAssembler(client)
|
||||
|
||||
val all =
|
||||
listOf(
|
||||
account,
|
||||
home,
|
||||
chatroomList,
|
||||
video,
|
||||
discovery,
|
||||
channelFinder,
|
||||
eventFinder,
|
||||
userFinder,
|
||||
search,
|
||||
channel,
|
||||
chatroom,
|
||||
community,
|
||||
thread,
|
||||
profile,
|
||||
hashtags,
|
||||
geohashes,
|
||||
nwc,
|
||||
)
|
||||
|
||||
fun start() = all.forEach { it.start() }
|
||||
|
||||
fun stop() = all.forEach { it.stop() }
|
||||
|
||||
fun destroy() = all.forEach { it.destroy() }
|
||||
|
||||
fun printCounters() = all.forEach { it.stats.printCounter() }
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
|
||||
@Composable
|
||||
fun RelaySubscriptionsCoordinatorSubscription() = RelaySubscriptionsCoordinatorSubscription(Amethyst.instance.sources)
|
||||
|
||||
@Composable
|
||||
fun RelaySubscriptionsCoordinatorSubscription(dataSource: RelaySubscriptionsCoordinator) {
|
||||
DisposableEffect(Unit) {
|
||||
dataSource.start()
|
||||
onDispose {
|
||||
dataSource.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand.account
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSERelayList
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.blossom.BlossomServersEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
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.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
class AccountQueryState(
|
||||
val account: Account,
|
||||
val otherAccounts: Set<HexKey>,
|
||||
)
|
||||
|
||||
class AccountFilterAssembler(
|
||||
client: NostrClient,
|
||||
) : QueryBasedSubscriptionOrchestrator<AccountQueryState>(client) {
|
||||
val latestEOSE = EOSERelayList()
|
||||
var hasLoadedTheBasics: Boolean = false
|
||||
|
||||
fun createAccountMetadataFilter(authorsHexes: List<String>): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
ContactListEvent.KIND,
|
||||
StatusEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND,
|
||||
SearchRelayListEvent.KIND,
|
||||
FileServersEvent.KIND,
|
||||
BlossomServersEvent.KIND,
|
||||
PrivateOutboxRelayListEvent.KIND,
|
||||
),
|
||||
authors = authorsHexes,
|
||||
limit = 20 * authorsHexes.size,
|
||||
),
|
||||
)
|
||||
|
||||
fun createOtherAccountsBaseFilter(otherAccounts: List<HexKey>): TypedFilter? {
|
||||
if (otherAccounts.isEmpty()) return null
|
||||
return TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
ContactListEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND,
|
||||
SearchRelayListEvent.KIND,
|
||||
FileServersEvent.KIND,
|
||||
BlossomServersEvent.KIND,
|
||||
MuteListEvent.KIND,
|
||||
PeopleListEvent.KIND,
|
||||
),
|
||||
authors = otherAccounts,
|
||||
limit = otherAccounts.size * 20,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createAccountSettingsFilter(authorsHexes: List<String>): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
PeopleListEvent.KIND,
|
||||
MuteListEvent.KIND,
|
||||
BadgeProfilesEvent.KIND,
|
||||
EmojiPackSelectionEvent.KIND,
|
||||
),
|
||||
authors = authorsHexes,
|
||||
limit = 100 * authorsHexes.size,
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountSettings2Filter(authorsHexes: List<String>): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
AppSpecificDataEvent.KIND,
|
||||
),
|
||||
authors = authorsHexes,
|
||||
tags = mapOf("d" to listOf(Account.APP_SPECIFIC_DATA_D_TAG)),
|
||||
limit = 2 * authorsHexes.size,
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountLastPostsListFilter(authorsHexes: List<String>): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
authors = authorsHexes,
|
||||
limit = 500,
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountReportsAndDraftsFilter(authorsHexes: List<String>): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
DraftEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
),
|
||||
authors = authorsHexes,
|
||||
since = latestEOSE.relayList,
|
||||
),
|
||||
)
|
||||
|
||||
fun createGiftWrapsToMeFilter(authorsHexes: List<String>) =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(GiftWrapEvent.KIND),
|
||||
tags = mapOf("p" to authorsHexes),
|
||||
since =
|
||||
latestEOSE.relayList.mapValues {
|
||||
EOSETime(it.value.time - TimeUtils.twoDays())
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
fun createNotificationFilter(authorsHexes: List<String>) =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
LnZapPaymentResponseEvent.KIND,
|
||||
ChannelMessageEvent.KIND,
|
||||
BadgeAwardEvent.KIND,
|
||||
),
|
||||
tags = mapOf("p" to authorsHexes),
|
||||
limit = 4000,
|
||||
since = latestEOSE.relayList,
|
||||
),
|
||||
)
|
||||
|
||||
fun createNotificationFilter2(authorsHexes: List<String>) =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
GitReplyEvent.KIND,
|
||||
GitIssueEvent.KIND,
|
||||
GitPatchEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
CalendarDateSlotEvent.KIND,
|
||||
CalendarTimeSlotEvent.KIND,
|
||||
CalendarRSVPEvent.KIND,
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
InteractiveStorySceneEvent.KIND,
|
||||
),
|
||||
tags = mapOf("p" to authorsHexes),
|
||||
limit = 400,
|
||||
since = latestEOSE.relayList,
|
||||
),
|
||||
)
|
||||
|
||||
fun mergeAllFilters(
|
||||
mainAccounts: List<HexKey>,
|
||||
otherAccounts: List<HexKey>,
|
||||
): List<TypedFilter>? =
|
||||
if (hasLoadedTheBasics) {
|
||||
// gets everything about the user logged in
|
||||
listOfNotNull(
|
||||
createAccountMetadataFilter(mainAccounts),
|
||||
createAccountSettings2Filter(mainAccounts),
|
||||
createNotificationFilter(mainAccounts),
|
||||
createNotificationFilter2(mainAccounts),
|
||||
createGiftWrapsToMeFilter(mainAccounts),
|
||||
createAccountReportsAndDraftsFilter(mainAccounts),
|
||||
createAccountSettingsFilter(mainAccounts),
|
||||
createAccountLastPostsListFilter(mainAccounts),
|
||||
createOtherAccountsBaseFilter(otherAccounts),
|
||||
).ifEmpty { null }
|
||||
} else {
|
||||
// just the basics.
|
||||
listOf(
|
||||
createAccountMetadataFilter(mainAccounts),
|
||||
createAccountSettingsFilter(mainAccounts),
|
||||
createAccountSettings2Filter(mainAccounts),
|
||||
).ifEmpty { null }
|
||||
}
|
||||
|
||||
val accountChannel =
|
||||
requestNewSubscription { time, relayUrl ->
|
||||
if (hasLoadedTheBasics) {
|
||||
latestEOSE.addOrUpdate(relayUrl, time)
|
||||
} else {
|
||||
hasLoadedTheBasics = true
|
||||
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
// One sub per subscribed account
|
||||
override fun updateSubscriptions(keys: Set<AccountQueryState>) {
|
||||
val mainAccounts = mutableSetOf<HexKey>()
|
||||
val otherAccounts = mutableSetOf<HexKey>()
|
||||
|
||||
keys.forEach {
|
||||
mainAccounts.add(it.account.userProfile().pubkeyHex)
|
||||
otherAccounts.addAll(it.otherAccounts)
|
||||
}
|
||||
|
||||
accountChannel.typedFilters = mergeAllFilters(mainAccounts.toList(), (otherAccounts - mainAccounts).toList())
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand.account
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun AccountFilterAssemblerSubscription(accountViewModel: AccountViewModel) = AccountFilterAssemblerSubscription(accountViewModel, Amethyst.instance.sources.account)
|
||||
|
||||
@Composable
|
||||
fun AccountFilterAssemblerSubscription(
|
||||
accountViewModel: AccountViewModel,
|
||||
dataSource: AccountFilterAssembler,
|
||||
) {
|
||||
// different screens get different states
|
||||
// even if they are tracking the same tag.
|
||||
val state =
|
||||
remember(accountViewModel) {
|
||||
AccountQueryState(accountViewModel.account, accountViewModel.allAccountsSync().toSet())
|
||||
}
|
||||
|
||||
KeyDataSourceSubscription(state, dataSource)
|
||||
}
|
||||
+32
-32
@@ -18,23 +18,32 @@
|
||||
* 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.service
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import kotlin.collections.filter
|
||||
import kotlin.collections.mapNotNull
|
||||
|
||||
object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed") {
|
||||
private var channelsToWatch = setOf<Channel>()
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
class ChannelFinderQueryState(
|
||||
val channel: Channel,
|
||||
)
|
||||
|
||||
private fun createMetadataChangeFilter(): TypedFilter? {
|
||||
val reactionsToWatch = channelsToWatch.filter { it is PublicChatChannel }.map { it.idHex }
|
||||
class ChannelFinderFilterAssembler(
|
||||
client: NostrClient,
|
||||
) : QueryBasedSubscriptionOrchestrator<ChannelFinderQueryState>(client) {
|
||||
private fun createMetadataChangeFilter(keys: Set<ChannelFinderQueryState>): TypedFilter? {
|
||||
val reactionsToWatch = keys.filter { it.channel is PublicChatChannel }.map { it.channel.idHex }
|
||||
|
||||
if (reactionsToWatch.isEmpty()) {
|
||||
return null
|
||||
@@ -51,11 +60,11 @@ object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed
|
||||
)
|
||||
}
|
||||
|
||||
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
|
||||
fun createLoadEventsIfNotLoadedFilter(keys: Set<ChannelFinderQueryState>): TypedFilter? {
|
||||
val directEventsToLoad =
|
||||
channelsToWatch.filter { it.notes.isEmpty() && it is PublicChatChannel }
|
||||
keys.filter { it.channel.notes.isEmpty() && it.channel is PublicChatChannel }
|
||||
|
||||
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
|
||||
val interestedEvents = (directEventsToLoad).map { it.channel.idHex }.toSet()
|
||||
|
||||
if (interestedEvents.isEmpty()) {
|
||||
return null
|
||||
@@ -72,11 +81,17 @@ object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed
|
||||
)
|
||||
}
|
||||
|
||||
fun createLoadStreamingIfNotLoadedFilter(): List<TypedFilter>? {
|
||||
fun createLoadStreamingIfNotLoadedFilter(keys: Set<ChannelFinderQueryState>): List<TypedFilter>? {
|
||||
val directEventsToLoad =
|
||||
channelsToWatch.filterIsInstance<LiveActivitiesChannel>().filter { it.info == null }
|
||||
keys.mapNotNull {
|
||||
if (it.channel is LiveActivitiesChannel && it.channel.info == null) {
|
||||
it.channel
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
|
||||
val interestedEvents = directEventsToLoad.map { it.idHex }.toSet()
|
||||
|
||||
if (interestedEvents.isEmpty()) {
|
||||
return null
|
||||
@@ -98,28 +113,13 @@ object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed
|
||||
}
|
||||
}
|
||||
|
||||
val singleChannelChannel = requestNewChannel()
|
||||
val singleChannelChannel = requestNewSubscription()
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
val reactions = createMetadataChangeFilter()
|
||||
val missing = createLoadEventsIfNotLoadedFilter()
|
||||
val missingStreaming = createLoadStreamingIfNotLoadedFilter()
|
||||
override fun updateSubscriptions(keys: Set<ChannelFinderQueryState>) {
|
||||
val reactions = createMetadataChangeFilter(keys)
|
||||
val missing = createLoadEventsIfNotLoadedFilter(keys)
|
||||
val missingStreaming = createLoadStreamingIfNotLoadedFilter(keys)
|
||||
|
||||
singleChannelChannel.typedFilters =
|
||||
((listOfNotNull(reactions, missing)) + (missingStreaming ?: emptyList())).ifEmpty { null }
|
||||
}
|
||||
|
||||
fun add(eventId: Channel) {
|
||||
if (eventId !in channelsToWatch) {
|
||||
channelsToWatch = channelsToWatch.plus(eventId)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(eventId: Channel) {
|
||||
if (eventId in channelsToWatch) {
|
||||
channelsToWatch = channelsToWatch.minus(eventId)
|
||||
invalidateFilters()
|
||||
}
|
||||
singleChannelChannel.typedFilters = ((listOfNotNull(reactions, missing)) + (missingStreaming ?: emptyList())).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand.channel
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
|
||||
|
||||
@Composable
|
||||
fun ChannelFinderFilterAssemblerSubscription(channel: Channel) = ChannelFinderFilterAssemblerSubscription(channel, Amethyst.instance.sources.channelFinder)
|
||||
|
||||
@Composable
|
||||
fun ChannelFinderFilterAssemblerSubscription(
|
||||
channel: Channel,
|
||||
dataSource: ChannelFinderFilterAssembler,
|
||||
) {
|
||||
// different screens get different states
|
||||
// even if they are tracking the same tag.
|
||||
val state =
|
||||
remember(channel) {
|
||||
ChannelFinderQueryState(channel)
|
||||
}
|
||||
|
||||
KeyDataSourceSubscription(state, dataSource)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand.channel
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.ChannelState
|
||||
|
||||
@Composable
|
||||
fun observeChannel(baseChannel: Channel): State<ChannelState?> {
|
||||
ChannelFinderFilterAssemblerSubscription(baseChannel)
|
||||
|
||||
return baseChannel.live.observeAsState()
|
||||
}
|
||||
+58
-89
@@ -18,12 +18,15 @@
|
||||
* 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.service
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
@@ -43,26 +46,22 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
|
||||
import kotlin.collections.mapNotNullTo
|
||||
|
||||
object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
private var nextEventsToWatch = setOf<Note>()
|
||||
private var nextAddressesToWatch = setOf<Note>()
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
class EventFinderQueryState(
|
||||
val note: Note,
|
||||
)
|
||||
|
||||
private var eventsToWatchInProd = setOf<Note>()
|
||||
private var addressesToWatchInProd = setOf<Note>()
|
||||
|
||||
private fun createReactionsToWatchInAddressFilter(): List<TypedFilter>? {
|
||||
val myAddressesToWatch =
|
||||
(
|
||||
eventsToWatchInProd.filter { it.address() != null } +
|
||||
addressesToWatchInProd.filter { it.address() != null }
|
||||
).toSet()
|
||||
|
||||
if (myAddressesToWatch.isEmpty()) {
|
||||
class EventFinderFilterAssembler(
|
||||
client: NostrClient,
|
||||
) : QueryBasedSubscriptionOrchestrator<EventFinderQueryState>(client) {
|
||||
private fun createReactionsToWatchInAddressFilter(keys: Set<AddressableNote>): List<TypedFilter>? {
|
||||
if (keys.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return groupByEOSEPresence(myAddressesToWatch)
|
||||
return groupByEOSEPresence(keys)
|
||||
.map {
|
||||
listOf(
|
||||
TypedFilter(
|
||||
@@ -105,15 +104,15 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
private fun createAddressFilter(): List<TypedFilter>? {
|
||||
val myAddressesToWatch = addressesToWatchInProd.filter { it.event == null }
|
||||
private fun createAddressFilter(keys: Set<AddressableNote>): List<TypedFilter>? {
|
||||
val myAddressesToWatch = keys.filter { it.event == null }
|
||||
|
||||
if (myAddressesToWatch.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return myAddressesToWatch.mapNotNull {
|
||||
it.address()?.let { aTag ->
|
||||
return myAddressesToWatch.map {
|
||||
it.address().let { aTag ->
|
||||
if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
@@ -140,12 +139,12 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
|
||||
if (eventsToWatchInProd.isEmpty()) {
|
||||
private fun createRepliesAndReactionsFilter(keys: Set<Note>): List<TypedFilter>? {
|
||||
if (keys.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return groupByEOSEPresence(eventsToWatchInProd)
|
||||
return groupByEOSEPresence(keys)
|
||||
.map {
|
||||
listOf(
|
||||
TypedFilter(
|
||||
@@ -191,12 +190,8 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
private fun createQuotesFilter(): List<TypedFilter>? {
|
||||
if (eventsToWatchInProd.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return groupByEOSEPresence(eventsToWatchInProd)
|
||||
private fun createQuotesFilter(keys: Set<Note>): List<TypedFilter>? =
|
||||
groupByEOSEPresence(keys)
|
||||
.map {
|
||||
listOf(
|
||||
TypedFilter(
|
||||
@@ -212,18 +207,17 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
),
|
||||
)
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
|
||||
val directEventsToLoad = eventsToWatchInProd.filter { it.event == null }
|
||||
fun createLoadEventsIfNotLoadedFilter(keys: Set<Note>): List<TypedFilter>? {
|
||||
val directEventsToLoad = keys.filter { it.event == null }
|
||||
|
||||
val threadingEventsToLoad =
|
||||
eventsToWatchInProd
|
||||
keys
|
||||
.mapNotNull { it.replyTo }
|
||||
.flatten()
|
||||
.filter { it !is AddressableNote && it.event == null }
|
||||
|
||||
val interestedEvents = (directEventsToLoad + threadingEventsToLoad).map { it.idHex }.toSet()
|
||||
val interestedEvents = (directEventsToLoad + threadingEventsToLoad).map { it.idHex }.toSet().toList()
|
||||
|
||||
if (interestedEvents.isEmpty()) {
|
||||
return null
|
||||
@@ -233,34 +227,22 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
return listOf(
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
ids = interestedEvents.toList(),
|
||||
),
|
||||
filter = SincePerRelayFilter(ids = interestedEvents),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val singleEventChannel =
|
||||
requestNewChannel { time, relayUrl ->
|
||||
requestNewSubscription { time, relayUrl ->
|
||||
// Ignores EOSE if it is in the middle of a filter change.
|
||||
if (changingFilters.get()) return@requestNewChannel
|
||||
if (isUpdatingFilters()) return@requestNewSubscription
|
||||
|
||||
checkNotInMainThread()
|
||||
|
||||
eventsToWatchInProd.forEach {
|
||||
val eose = it.lastReactionsDownloadTime[relayUrl]
|
||||
forEachSubscriber {
|
||||
val eose = it.note.lastReactionsDownloadTime[relayUrl]
|
||||
if (eose == null) {
|
||||
it.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time))
|
||||
} else {
|
||||
eose.time = time
|
||||
}
|
||||
}
|
||||
|
||||
addressesToWatchInProd.forEach {
|
||||
val eose = it.lastReactionsDownloadTime[relayUrl]
|
||||
if (eose == null) {
|
||||
it.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time))
|
||||
it.note.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time))
|
||||
} else {
|
||||
eose.time = time
|
||||
}
|
||||
@@ -271,46 +253,33 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
invalidateFilters()
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
addressesToWatchInProd = nextAddressesToWatch
|
||||
eventsToWatchInProd = nextEventsToWatch
|
||||
override fun updateSubscriptions(keys: Set<EventFinderQueryState>) {
|
||||
val addressables =
|
||||
keys
|
||||
.mapNotNullTo(mutableSetOf()) {
|
||||
it.note as? AddressableNote
|
||||
}.toSet()
|
||||
|
||||
val reactions = createRepliesAndReactionsFilter()
|
||||
val missing = createLoadEventsIfNotLoadedFilter()
|
||||
val addresses = createAddressFilter()
|
||||
val addressReactions = createReactionsToWatchInAddressFilter()
|
||||
val quotes = createQuotesFilter()
|
||||
val events =
|
||||
keys
|
||||
.mapNotNullTo(mutableSetOf()) {
|
||||
if (it.note !is AddressableNote) {
|
||||
it.note
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toSet()
|
||||
|
||||
val eventReactions = createRepliesAndReactionsFilter(events)
|
||||
val addressReactions = createReactionsToWatchInAddressFilter(addressables)
|
||||
|
||||
val missingEvents = createLoadEventsIfNotLoadedFilter(events)
|
||||
val missingAddresses = createAddressFilter(addressables)
|
||||
|
||||
val quotes = createQuotesFilter(events + addressables)
|
||||
|
||||
singleEventChannel.typedFilters =
|
||||
listOfNotNull(missing, addresses, reactions, addressReactions, quotes).flatten().ifEmpty { null }
|
||||
}
|
||||
|
||||
fun add(eventId: Note) {
|
||||
if (!nextEventsToWatch.contains(eventId)) {
|
||||
nextEventsToWatch = nextEventsToWatch.plus(eventId)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(eventId: Note) {
|
||||
if (nextEventsToWatch.contains(eventId)) {
|
||||
nextEventsToWatch = nextEventsToWatch.minus(eventId)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun addAddress(addressableNote: Note) {
|
||||
if (!nextAddressesToWatch.contains(addressableNote)) {
|
||||
nextAddressesToWatch = nextAddressesToWatch.plus(addressableNote)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAddress(addressableNote: Note) {
|
||||
if (nextAddressesToWatch.contains(addressableNote)) {
|
||||
nextAddressesToWatch = nextAddressesToWatch.minus(addressableNote)
|
||||
invalidateFilters()
|
||||
}
|
||||
listOfNotNull(missingEvents, missingAddresses, eventReactions, addressReactions, quotes).flatten().ifEmpty { null }
|
||||
}
|
||||
}
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
|
||||
|
||||
@Composable
|
||||
fun EventFinderFilterAssemblerSubscription(note: Note) = EventFinderFilterAssemblerSubscription(note, Amethyst.instance.sources.eventFinder)
|
||||
|
||||
@Composable
|
||||
fun EventFinderFilterAssemblerSubscription(
|
||||
note: Note,
|
||||
dataSource: EventFinderFilterAssembler,
|
||||
) {
|
||||
// different screens get different states
|
||||
// even if they are tracking the same tag.
|
||||
val state =
|
||||
remember(note) {
|
||||
EventFinderQueryState(note)
|
||||
}
|
||||
|
||||
KeyDataSourceSubscription(state, dataSource)
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.note.combineWith
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
|
||||
@Composable
|
||||
fun observeNote(note: Note): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note.live().metadata.observeAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T : Event> observeNoteEvent(note: Note): State<T?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.note.event as? T? }
|
||||
.observeAsState(note.event as? T?)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> observeNoteAndMap(
|
||||
note: Note,
|
||||
map: (Note) -> T,
|
||||
): State<T> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note
|
||||
.live()
|
||||
.metadata
|
||||
.map { map(it.note) }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(map(note))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T, U> observeNoteEventAndMap(
|
||||
note: Note,
|
||||
map: (T) -> U,
|
||||
): State<U?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note
|
||||
.live()
|
||||
.metadata
|
||||
.map { (it.note.event as? T)?.let { map(it) } }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
(note.event as? T)?.let { map(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteHasEvent(note: Note): State<Boolean> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.note.event != null }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(note.event != null)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReplies(note: Note): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note.live().replies.observeAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReplyCount(note: Note): State<Int> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note
|
||||
.live()
|
||||
.replies
|
||||
.map { it.note.reactions.size }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(note.reactions.size)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReactions(note: Note): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note.live().reactions.observeAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReactionCount(note: Note): State<Int> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note
|
||||
.live()
|
||||
.reactions
|
||||
.map {
|
||||
var total = 0
|
||||
it.note.reactions.forEach { total += it.value.size }
|
||||
total
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(0)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteZaps(note: Note): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note.live().zaps.observeAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReposts(note: Note): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note.live().boosts.observeAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteRepostsBy(
|
||||
note: Note,
|
||||
user: User,
|
||||
): State<Boolean> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note
|
||||
.live()
|
||||
.boosts
|
||||
.map { it.note.isBoostedBy(user) }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(note.isBoostedBy(user))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteRepostCount(note: Note): State<Int> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note
|
||||
.live()
|
||||
.boosts
|
||||
.map { it.note.boosts.size }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(note.boosts.size)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReferences(note: Note): State<Boolean> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note
|
||||
.live()
|
||||
.zaps
|
||||
.combineWith(note.live().boosts, note.live().reactions) { zapState, boostState, reactionState ->
|
||||
zapState?.note?.zaps?.isNotEmpty() == true ||
|
||||
boostState?.note?.boosts?.isNotEmpty() == true ||
|
||||
reactionState?.note?.reactions?.isNotEmpty() == true
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
note.zaps.isNotEmpty() || note.boosts.isNotEmpty() || note.reactions.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteOts(note: Note): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note.live().ots.observeAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteEdits(note: Note): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return note.live().edits.observeAsState()
|
||||
}
|
||||
+21
-22
@@ -18,45 +18,44 @@
|
||||
* 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.service
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
|
||||
class NostrLnZapPaymentResponseDataSource(
|
||||
private val fromServiceHex: String,
|
||||
private val toUserHex: String,
|
||||
private val replyingToHex: String,
|
||||
private val authSigner: NostrSigner,
|
||||
) : AmethystNostrDataSource("LnZapPaymentResponseFeed") {
|
||||
val feedTypes = setOf(FeedType.WALLET_CONNECT)
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
class NWCPaymentQueryState(
|
||||
val fromServiceHex: String,
|
||||
val toUserHex: String,
|
||||
val replyingToHex: String,
|
||||
)
|
||||
|
||||
private fun createWalletConnectServiceWatcher(): TypedFilter {
|
||||
// downloads all the reactions to a given event.
|
||||
return TypedFilter(
|
||||
types = feedTypes,
|
||||
class NWCPaymentFilterAssembler(
|
||||
client: NostrClient,
|
||||
) : QueryBasedSubscriptionOrchestrator<NWCPaymentQueryState>(client) {
|
||||
fun createAccountMetadataFilter(keys: Set<NWCPaymentQueryState>): TypedFilter =
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.WALLET_CONNECT),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LnZapPaymentResponseEvent.KIND),
|
||||
authors = listOf(fromServiceHex),
|
||||
authors = keys.map { it.fromServiceHex },
|
||||
tags =
|
||||
mapOf(
|
||||
"e" to listOf(replyingToHex),
|
||||
"p" to listOf(toUserHex),
|
||||
"e" to keys.map { it.replyingToHex },
|
||||
"p" to keys.map { it.toUserHex },
|
||||
),
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val channel = requestNewChannel()
|
||||
val channel = requestNewSubscription()
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
val wc = createWalletConnectServiceWatcher()
|
||||
|
||||
channel.typedFilters = listOfNotNull(wc).ifEmpty { null }
|
||||
override fun updateSubscriptions(keys: Set<NWCPaymentQueryState>) {
|
||||
channel.typedFilters = listOf(createAccountMetadataFilter(keys))
|
||||
}
|
||||
}
|
||||
+33
-35
@@ -18,10 +18,14 @@
|
||||
* 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.service
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user
|
||||
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.findMinimumEOSEsForUsers
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.groupByEOSEPresence
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
@@ -32,13 +36,18 @@ import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
|
||||
object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
private var usersToWatch = setOf<User>()
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
class UserFinderQueryState(
|
||||
val user: User,
|
||||
)
|
||||
|
||||
fun createUserMetadataFilter(): List<TypedFilter>? {
|
||||
if (usersToWatch.isEmpty()) return null
|
||||
class UserFinderFilterAssembler(
|
||||
client: NostrClient,
|
||||
) : QueryBasedSubscriptionOrchestrator<UserFinderQueryState>(client) {
|
||||
fun createUserMetadataFilter(keys: Set<UserFinderQueryState>): List<TypedFilter>? {
|
||||
if (keys.isEmpty()) return null
|
||||
|
||||
val firstTimers = usersToWatch.filter { it.latestMetadata == null }.map { it.pubkeyHex }
|
||||
val firstTimers = keys.filter { it.user.latestMetadata == null }.map { it.user.pubkeyHex }.distinct()
|
||||
|
||||
if (firstTimers.isEmpty()) return null
|
||||
|
||||
@@ -54,10 +63,10 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
)
|
||||
}
|
||||
|
||||
fun createUserMetadataStatusReportFilter(): List<TypedFilter>? {
|
||||
if (usersToWatch.isEmpty()) return null
|
||||
fun createUserMetadataStatusReportFilter(keys: Set<UserFinderQueryState>): List<TypedFilter>? {
|
||||
if (keys.isEmpty()) return null
|
||||
|
||||
val secondTimers = usersToWatch.filter { it.latestMetadata != null }
|
||||
val secondTimers = keys.filter { it.user.latestMetadata != null }.map { it.user }.toSet()
|
||||
|
||||
if (secondTimers.isEmpty()) return null
|
||||
|
||||
@@ -72,7 +81,14 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, RelationshipStatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND),
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
StatusEvent.KIND,
|
||||
RelationshipStatusEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND,
|
||||
),
|
||||
authors = groupIds,
|
||||
since = minEOSEs,
|
||||
),
|
||||
@@ -94,41 +110,23 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
}
|
||||
|
||||
val userChannel =
|
||||
requestNewChannel { time, relayUrl ->
|
||||
checkNotInMainThread()
|
||||
|
||||
usersToWatch.forEach {
|
||||
val eose = it.latestEOSEs[relayUrl]
|
||||
requestNewSubscription { time, relayUrl ->
|
||||
forEachSubscriber {
|
||||
val eose = it.user.latestEOSEs[relayUrl]
|
||||
if (eose == null) {
|
||||
it.latestEOSEs = it.latestEOSEs + Pair(relayUrl, EOSETime(time))
|
||||
it.user.latestEOSEs = it.user.latestEOSEs + Pair(relayUrl, EOSETime(time))
|
||||
} else {
|
||||
eose.time = time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
checkNotInMainThread()
|
||||
|
||||
override fun updateSubscriptions(keys: Set<UserFinderQueryState>) {
|
||||
userChannel.typedFilters =
|
||||
listOfNotNull(
|
||||
createUserMetadataFilter(),
|
||||
createUserMetadataStatusReportFilter(),
|
||||
createUserMetadataFilter(keys),
|
||||
createUserMetadataStatusReportFilter(keys),
|
||||
).flatten()
|
||||
.ifEmpty { null }
|
||||
}
|
||||
|
||||
fun add(user: User) {
|
||||
if (!usersToWatch.contains(user)) {
|
||||
usersToWatch = usersToWatch.plus(user)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(user: User) {
|
||||
if (usersToWatch.contains(user)) {
|
||||
usersToWatch = usersToWatch.minus(user)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand.user
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
|
||||
|
||||
@Composable
|
||||
fun UserFinderFilterAssemblerSubscription(user: User) = UserFinderFilterAssemblerSubscription(user, Amethyst.instance.sources.userFinder)
|
||||
|
||||
@Composable
|
||||
fun UserFinderFilterAssemblerSubscription(
|
||||
user: User,
|
||||
dataSource: UserFinderFilterAssembler,
|
||||
) {
|
||||
// different screens get different states
|
||||
// even if they are tracking the same tag.
|
||||
val state =
|
||||
remember(user) {
|
||||
UserFinderQueryState(user)
|
||||
}
|
||||
|
||||
KeyDataSourceSubscription(state, dataSource)
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.reqCommand.user
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
|
||||
@Composable
|
||||
fun observeUser(user: User): State<UserState?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user.live().metadata.observeAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeUserName(user: User): State<String> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.user.toBestDisplayName() }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(user.toBestDisplayName())
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeUserNip05(user: User): State<String?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.user.info?.nip05 }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(user.info?.nip05)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeUserAboutMe(user: User): State<String> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.user.info?.about ?: "" }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(user.info?.about ?: "")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeUserInfo(user: User): State<UserMetadata?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.user.info }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(user.info)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeUserBanner(user: User): State<String> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.user.info?.banner ?: "" }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(user.info?.banner ?: "")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeUserPicture(user: User): State<String?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.user.info?.picture }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(user.info?.picture)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeUserShortName(user: User): State<String> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.user.toBestShortFirstName() }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(user.toBestShortFirstName())
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.searchCommand
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.datasources.SubscriptionOrchestrator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
interface MutableQueryState {
|
||||
fun flow(): Flow<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a data source where the filters are derived from
|
||||
* data (keys) that is being watched in the UI.
|
||||
*/
|
||||
abstract class MutableQueryBasedSubscriptionOrchestrator<T : MutableQueryState>(
|
||||
client: NostrClient,
|
||||
val scope: CoroutineScope,
|
||||
) : SubscriptionOrchestrator(client) {
|
||||
private var queries: ConcurrentHashMap<T, Job?> = ConcurrentHashMap()
|
||||
|
||||
// This is called by main. Keep it really fast.
|
||||
fun subscribe(query: T?) {
|
||||
if (query == null) return
|
||||
|
||||
val wasEmpty = queries.isEmpty()
|
||||
|
||||
queries[query]?.cancel()
|
||||
queries[query] =
|
||||
scope.launch {
|
||||
query.flow().collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
if (wasEmpty) {
|
||||
start()
|
||||
}
|
||||
|
||||
invalidateFilters()
|
||||
|
||||
if (isDebug) {
|
||||
Log.d(this::class.simpleName, "Watch $query (${queries.size} queries)")
|
||||
}
|
||||
}
|
||||
|
||||
// This is called by main. Keep it really fast.
|
||||
fun unsubscribe(query: T?) {
|
||||
if (query == null) return
|
||||
|
||||
queries[query]?.cancel()
|
||||
queries.remove(query)
|
||||
|
||||
invalidateFilters()
|
||||
|
||||
if (queries.isEmpty()) {
|
||||
stop()
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
Log.d(this::class.simpleName, "Unwatch $query (${queries.size} queries)")
|
||||
}
|
||||
}
|
||||
|
||||
fun forEachSubscriber(action: (T) -> Unit) {
|
||||
queries.keys.forEach(action)
|
||||
}
|
||||
|
||||
final override fun updateSubscriptions() {
|
||||
updateSubscriptions(queries.keys)
|
||||
}
|
||||
|
||||
abstract fun updateSubscriptions(keys: Set<T>)
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.searchCommand
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
|
||||
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Note
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlin.collections.flatten
|
||||
|
||||
@Stable
|
||||
class SearchQueryState(
|
||||
val searchQuery: MutableStateFlow<String>,
|
||||
) : MutableQueryState {
|
||||
override fun flow(): Flow<String> = searchQuery
|
||||
}
|
||||
|
||||
class SearchFilterAssembler(
|
||||
val cache: LocalCache,
|
||||
client: NostrClient,
|
||||
scope: CoroutineScope,
|
||||
) : MutableQueryBasedSubscriptionOrchestrator<SearchQueryState>(client, scope) {
|
||||
fun filterByAuthor(pubKey: HexKey) =
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = ALL_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOfNotNull(pubKey),
|
||||
limit = 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
fun filterByEvent(eventId: HexKey) =
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = ALL_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
ids = listOfNotNull(eventId),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
fun filterByAddress(parsed: NAddress) =
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = ALL_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOfNotNull(parsed.author),
|
||||
limit = 1,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = ALL_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(parsed.kind),
|
||||
authors = listOfNotNull(parsed.author),
|
||||
tags = mapOf("d" to listOf(parsed.dTag)),
|
||||
limit = 5,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun createAnythingWithIDFilter(keys: Set<SearchQueryState>): List<TypedFilter> {
|
||||
if (keys.isEmpty()) return emptyList()
|
||||
|
||||
val uniqueQueries =
|
||||
keys.mapNotNullTo(mutableSetOf()) {
|
||||
if (!it.searchQuery.value.isBlank()) {
|
||||
it.searchQuery.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueQueries
|
||||
.map { mySearchString ->
|
||||
val directFilters =
|
||||
runCatching {
|
||||
if (Hex.isHex(mySearchString)) {
|
||||
val key = Hex.decode(mySearchString).toHexKey()
|
||||
filterByAuthor(key) + filterByEvent(key)
|
||||
} else {
|
||||
when (val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity) {
|
||||
is NSec -> filterByAuthor(Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey())
|
||||
is NPub -> filterByAuthor(parsed.hex)
|
||||
is NProfile -> filterByAuthor(parsed.hex)
|
||||
is Note -> filterByEvent(parsed.hex)
|
||||
is NEvent -> filterByEvent(parsed.hex)
|
||||
is NEmbed -> {
|
||||
cache.verifyAndConsume(parsed.event, null)
|
||||
emptyList()
|
||||
}
|
||||
|
||||
is NRelay -> emptyList()
|
||||
is NAddress -> filterByAddress(parsed)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}.getOrDefault(emptyList())
|
||||
|
||||
val searchFilters =
|
||||
listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
search = mySearchString,
|
||||
limit = 1000,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
BadgeDefinitionEvent.KIND,
|
||||
PeopleListEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
AudioTrackEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
ChannelCreateEvent.KIND,
|
||||
),
|
||||
search = mySearchString,
|
||||
limit = 100,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
ChannelMetadataEvent.KIND,
|
||||
ClassifiedsEvent.KIND,
|
||||
CommunityDefinitionEvent.KIND,
|
||||
EmojiPackEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
LiveActivitiesEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
NNSEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
),
|
||||
search = mySearchString,
|
||||
limit = 100,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
InteractiveStorySceneEvent.KIND,
|
||||
),
|
||||
search = mySearchString,
|
||||
limit = 100,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
directFilters + searchFilters
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
val searchChannel = requestNewSubscription()
|
||||
|
||||
override fun updateSubscriptions(keys: Set<SearchQueryState>) {
|
||||
searchChannel.typedFilters =
|
||||
logTime(
|
||||
debugMessage = { "Search DataSource UpdateSubscriptions with ${it?.size} filter size" },
|
||||
block = { createAnythingWithIDFilter(keys).ifEmpty { null } },
|
||||
)
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.searchCommand
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel
|
||||
|
||||
@Composable
|
||||
fun TextSearchDataSourceSubscription(searchBarViewModel: SearchBarViewModel) = TextSearchDataSourceSubscription(searchBarViewModel, Amethyst.instance.sources.search)
|
||||
|
||||
@Composable
|
||||
fun TextSearchDataSourceSubscription(
|
||||
searchBarViewModel: SearchBarViewModel,
|
||||
dataSource: SearchFilterAssembler,
|
||||
) {
|
||||
KeyDataSourceSubscription(searchBarViewModel.searchDataSourceState, dataSource)
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.relayClient.searchCommand
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
|
||||
@Composable
|
||||
fun UserSearchDataSourceSubscription(userSuggestions: UserSuggestionState) = UserSearchDataSourceSubscription(userSuggestions, Amethyst.instance.sources.search)
|
||||
|
||||
@Composable
|
||||
fun UserSearchDataSourceSubscription(
|
||||
userSuggestions: UserSuggestionState,
|
||||
dataSource: SearchFilterAssembler,
|
||||
) {
|
||||
KeyDataSourceSubscription(userSuggestions.searchDataSourceState, dataSource)
|
||||
}
|
||||
@@ -102,7 +102,10 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) { LanguageTranslatorService.clear() }
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) { debugState(this@MainActivity) }
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
debugState(this@MainActivity)
|
||||
Amethyst.instance.sources.printCounters()
|
||||
}
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -84,7 +83,6 @@ import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
@@ -135,15 +133,6 @@ fun EditPostView(
|
||||
postViewModel.load(edit, versionLookingAt, accountViewModel)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
NostrSearchEventOrUserDataSource.start()
|
||||
|
||||
onDispose {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
NostrSearchEventOrUserDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
properties =
|
||||
|
||||
@@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
@@ -251,8 +250,6 @@ open class EditPostViewModel : ViewModel() {
|
||||
|
||||
userSuggestions?.reset()
|
||||
userSuggestionsMainMessage = null
|
||||
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
}
|
||||
|
||||
open fun findUrlInMessage(): String? =
|
||||
|
||||
-455
@@ -1,455 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.actions
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun JoinUserOrChannelView(
|
||||
onClose: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val searchBarViewModel: SearchBarViewModel =
|
||||
viewModel(
|
||||
key = "SearchBarViewModel",
|
||||
factory =
|
||||
SearchBarViewModel.Factory(
|
||||
accountViewModel.account,
|
||||
),
|
||||
)
|
||||
|
||||
JoinUserOrChannelView(
|
||||
searchBarViewModel = searchBarViewModel,
|
||||
onClose = onClose,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun JoinUserOrChannelView(
|
||||
searchBarViewModel: SearchBarViewModel,
|
||||
onClose: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
searchBarViewModel.clear()
|
||||
onClose()
|
||||
},
|
||||
properties =
|
||||
DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
),
|
||||
) {
|
||||
Surface {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(10.dp)
|
||||
.heightIn(min = 500.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CloseButton(
|
||||
onPress = {
|
||||
searchBarViewModel.clear()
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
onClose()
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.channel_list_join_conversation),
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
RenderSearch(searchBarViewModel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderSearch(
|
||||
searchBarViewModel: SearchBarViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
// Create a channel for processing search queries.
|
||||
val searchTextChanges = remember { Channel<String>(Channel.CONFLATED) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
checkNotInMainThread()
|
||||
if (searchBarViewModel.isSearchingFun()) {
|
||||
searchBarViewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// Wait for text changes to stop for 300 ms before firing off search.
|
||||
withContext(Dispatchers.IO) {
|
||||
searchTextChanges
|
||||
.receiveAsFlow()
|
||||
.filter { it.isNotBlank() }
|
||||
.distinctUntilChanged()
|
||||
.debounce(300)
|
||||
.collectLatest {
|
||||
if (it.length >= 2) {
|
||||
NostrSearchEventOrUserDataSource.search(it.trim())
|
||||
}
|
||||
|
||||
searchBarViewModel.invalidateData()
|
||||
|
||||
// makes sure to show the top of the search
|
||||
launch(Dispatchers.Main) { listState.animateScrollToItem(0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(lifeCycleOwner) {
|
||||
val observer =
|
||||
LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Join Start")
|
||||
NostrSearchEventOrUserDataSource.start()
|
||||
searchBarViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Join Stop")
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
NostrSearchEventOrUserDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
|
||||
// LAST ROW
|
||||
SearchEditTextForJoin(searchBarViewModel, searchTextChanges)
|
||||
|
||||
RenderSearchResults(searchBarViewModel, listState, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun SearchEditTextForJoin(
|
||||
searchBarViewModel: SearchBarViewModel,
|
||||
searchTextChanges: Channel<String>,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// initialize focus reference to be able to request focus programmatically
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
delay(100)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.channel_list_user_or_group_id)) },
|
||||
value = searchBarViewModel.searchValue,
|
||||
onValueChange = {
|
||||
searchBarViewModel.updateSearchValue(it)
|
||||
scope.launch(Dispatchers.IO) { searchTextChanges.trySend(it) }
|
||||
},
|
||||
leadingIcon = { SearchIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText) },
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f, true)
|
||||
.defaultMinSize(minHeight = 20.dp)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
keyboardController?.show()
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.channel_list_user_or_group_id_demo),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (searchBarViewModel.isSearching) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
searchBarViewModel.clear()
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Clear,
|
||||
contentDescription = stringRes(R.string.clear),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderSearchResults(
|
||||
searchBarViewModel: SearchBarViewModel,
|
||||
listState: LazyListState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (searchBarViewModel.isSearching) {
|
||||
val users by searchBarViewModel.searchResultsUsers.collectAsStateWithLifecycle()
|
||||
val channels by searchBarViewModel.searchResultsChannels.collectAsStateWithLifecycle()
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 10.dp),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
users,
|
||||
key = { _, item -> "u" + item.pubkeyHex },
|
||||
) { _, item ->
|
||||
UserComposeForChat(item, accountViewModel) {
|
||||
accountViewModel.createChatRoomFor(item) { nav.nav(Route.Room(it)) }
|
||||
|
||||
searchBarViewModel.clear()
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
channels,
|
||||
key = { _, item -> "c" + item.idHex },
|
||||
) { _, item ->
|
||||
RenderChannel(
|
||||
item,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
) {
|
||||
nav.nav(Route.Channel(item.idHex))
|
||||
searchBarViewModel.clear()
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderChannel(
|
||||
item: com.vitorpamplona.amethyst.model.Channel,
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ChannelName(
|
||||
channelIdHex = item.idHex,
|
||||
channelPicture = item.profilePicture(),
|
||||
channelTitle = {
|
||||
Text(
|
||||
item.toBestDisplayName(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
},
|
||||
channelLastTime = null,
|
||||
channelLastContent = item.summary(),
|
||||
hasNewMessages = false,
|
||||
onClick = onClick,
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
loadRobohash = loadRobohash,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserComposeForChat(
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
).padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp,
|
||||
bottom = 10.dp,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ClickableUserPicture(baseUser, Size55dp, accountViewModel)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser, accountViewModel = accountViewModel) }
|
||||
|
||||
DisplayUserAboutInfo(baseUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayUserAboutInfo(baseUser: User) {
|
||||
val baseUserState by baseUser.live().metadata.observeAsState()
|
||||
val about by remember(baseUserState) { derivedStateOf { baseUserState?.user?.info?.about ?: "" } }
|
||||
|
||||
Text(
|
||||
text = about,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import android.R.attr.category
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -45,7 +44,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||
@@ -1106,8 +1104,6 @@ open class NewPostViewModel :
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
draftTag = UUID.randomUUID().toString()
|
||||
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
}
|
||||
|
||||
fun deleteDraft() {
|
||||
|
||||
@@ -32,7 +32,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -58,6 +57,9 @@ import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
@@ -190,8 +192,7 @@ private fun DisplayNoteLink(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by it.live().metadata.observeAsState()
|
||||
|
||||
val noteState by observeNote(it)
|
||||
val note = remember(noteState) { noteState?.note } ?: return
|
||||
|
||||
val channelHex = remember(noteState) { note.channelHex() }
|
||||
@@ -214,7 +215,7 @@ private fun DisplayNoteLink(
|
||||
)
|
||||
} else if (channelHex != null) {
|
||||
LoadChannel(baseChannelHex = channelHex, accountViewModel) { baseChannel ->
|
||||
val channelState by baseChannel.live.observeAsState()
|
||||
val channelState by observeChannel(baseChannel)
|
||||
val channelDisplayName by
|
||||
remember(channelState) {
|
||||
derivedStateOf { channelState?.channel?.toBestDisplayName() ?: noteIdDisplayNote }
|
||||
@@ -254,7 +255,7 @@ private fun DisplayAddress(
|
||||
}
|
||||
|
||||
noteBase?.let {
|
||||
val noteState by it.live().metadata.observeAsState()
|
||||
val noteState by observeNote(it)
|
||||
|
||||
val route = remember(noteState) { Route.Note(nip19.aTag()) }
|
||||
val displayName = remember(noteState) { "@${noteState?.note?.idDisplayNote()}" }
|
||||
@@ -324,7 +325,7 @@ public fun RenderUserAsClickableText(
|
||||
additionalChars: String?,
|
||||
nav: INav,
|
||||
) {
|
||||
val userState by baseUser.live().userMetadataInfo.observeAsState()
|
||||
val userState by observeUserInfo(baseUser)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = userState?.bestName() ?: ("@" + baseUser.pubkeyDisplayHex()),
|
||||
|
||||
@@ -40,7 +40,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -86,6 +85,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
||||
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
@@ -777,7 +777,7 @@ private fun DisplayUserFromTag(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val meta by baseUser.live().userMetadataInfo.observeAsState(baseUser.info)
|
||||
val meta by observeUserInfo(baseUser)
|
||||
|
||||
CrossfadeIfEnabled(targetState = meta, label = "DisplayUserFromTag", accountViewModel = accountViewModel) {
|
||||
Row {
|
||||
|
||||
+2
-2
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
@@ -39,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.model.HashtagIcon
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayFullNote
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayUser
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
|
||||
@@ -231,7 +231,7 @@ class MarkdownMediaRenderer(
|
||||
) {
|
||||
renderInvisible(richTextStringBuilder) {
|
||||
// Preloads note if not loaded yet.
|
||||
baseNote.live().metadata.observeAsState()
|
||||
EventFinderFilterAssemblerSubscription(baseNote)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.dal
|
||||
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
|
||||
abstract fun applyFilter(collection: Set<T>): Set<T>
|
||||
|
||||
abstract fun sort(collection: Set<T>): List<T>
|
||||
|
||||
open fun updateListWith(
|
||||
oldList: List<T>,
|
||||
newItems: Set<T>,
|
||||
): List<T> {
|
||||
val (feed, elapsed) =
|
||||
measureTimedValue {
|
||||
val newItemsToBeAdded = applyFilter(newItems)
|
||||
if (newItemsToBeAdded.isNotEmpty()) {
|
||||
val newList = oldList.toSet() + newItemsToBeAdded
|
||||
sort(newList).take(limit())
|
||||
} else {
|
||||
oldList
|
||||
}
|
||||
}
|
||||
|
||||
// Log.d("Time", "${this.javaClass.simpleName} Additive Feed in $elapsed with ${feed.size}
|
||||
// objects")
|
||||
return feed
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
abstract class FeedFilter<T> {
|
||||
fun loadTop(): List<T> {
|
||||
checkNotInMainThread()
|
||||
|
||||
val (feed, elapsed) = measureTimedValue { feed() }
|
||||
|
||||
Log.d("Time", "${this.javaClass.simpleName} Full Feed in $elapsed with ${feed.size} objects")
|
||||
@@ -43,31 +40,3 @@ abstract class FeedFilter<T> {
|
||||
|
||||
abstract fun feed(): List<T>
|
||||
}
|
||||
|
||||
abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
|
||||
abstract fun applyFilter(collection: Set<T>): Set<T>
|
||||
|
||||
abstract fun sort(collection: Set<T>): List<T>
|
||||
|
||||
open fun updateListWith(
|
||||
oldList: List<T>,
|
||||
newItems: Set<T>,
|
||||
): List<T> {
|
||||
checkNotInMainThread()
|
||||
|
||||
val (feed, elapsed) =
|
||||
measureTimedValue {
|
||||
val newItemsToBeAdded = applyFilter(newItems)
|
||||
if (newItemsToBeAdded.isNotEmpty()) {
|
||||
val newList = oldList.toSet() + newItemsToBeAdded
|
||||
sort(newList).take(limit())
|
||||
} else {
|
||||
oldList
|
||||
}
|
||||
}
|
||||
|
||||
// Log.d("Time", "${this.javaClass.simpleName} Additive Feed in $elapsed with ${feed.size}
|
||||
// objects")
|
||||
return feed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.feeds
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
|
||||
interface InvalidatableContent {
|
||||
fun invalidateData(ignoreIfDoing: Boolean = false)
|
||||
|
||||
val isRefreshing: MutableState<Boolean>
|
||||
val isRefreshing: State<Boolean>
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.feeds
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
|
||||
@Composable
|
||||
fun WatchLifecycleAndUpdateModel(model: InvalidatableContent) {
|
||||
LaunchedEffect(model) {
|
||||
model.invalidateData(true)
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(model) {
|
||||
model.invalidateData(true)
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -18,7 +18,7 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.ui.layouts
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
@@ -53,6 +53,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import com.vitorpamplona.amethyst.model.BooleanType
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import kotlin.math.abs
|
||||
|
||||
+4
-3
@@ -45,7 +45,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -61,6 +60,8 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
@@ -214,7 +215,7 @@ private fun AccountPicture(
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
) {
|
||||
val profilePicture by user.live().profilePictureChanges.observeAsState()
|
||||
val profilePicture by observeUserPicture(user)
|
||||
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = user.pubkeyHex,
|
||||
@@ -231,7 +232,7 @@ private fun AccountName(
|
||||
acc: AccountInfo,
|
||||
user: User,
|
||||
) {
|
||||
val info by user.live().userMetadataInfo.observeAsState()
|
||||
val info by observeUserInfo(user)
|
||||
|
||||
info?.let {
|
||||
it.bestName()?.let { name ->
|
||||
|
||||
@@ -46,15 +46,14 @@ import androidx.core.util.Consumer
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataScreen
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayNotifyMessages
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.DisplayErrorMessages
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NewPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen
|
||||
@@ -72,6 +71,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
|
||||
|
||||
@@ -29,13 +29,13 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
@@ -89,12 +89,7 @@ private fun LoggedInUserPictureDrawer(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
IconButton(onClick = onClick) {
|
||||
val profilePicture by
|
||||
accountViewModel.account
|
||||
.userProfile()
|
||||
.live()
|
||||
.profilePictureChanges
|
||||
.observeAsState()
|
||||
val profilePicture by observeUserPicture(accountViewModel.userProfile())
|
||||
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = accountViewModel.userProfile().pubkeyHex,
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
|
||||
import android.R.attr.maxLines
|
||||
import android.R.attr.onClick
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -92,12 +90,14 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
@@ -183,7 +183,7 @@ fun ProfileContent(
|
||||
accountViewModel: AccountViewModel,
|
||||
onClickUser: () -> Unit,
|
||||
) {
|
||||
val userInfo by baseAccountUser.live().userMetadataInfo.observeAsState()
|
||||
val userInfo by observeUserInfo(baseAccountUser)
|
||||
|
||||
ProfileContentTemplate(
|
||||
profilePubHex = baseAccountUser.pubkeyHex,
|
||||
@@ -270,9 +270,9 @@ private fun EditStatusBoxes(
|
||||
StatusEditBar(accountViewModel = accountViewModel, nav = nav)
|
||||
} else {
|
||||
statuses.forEach {
|
||||
val originalStatus by it.live().content.observeAsState()
|
||||
val noteStatus by observeNote(it)
|
||||
|
||||
StatusEditBar(originalStatus, it.address, accountViewModel, nav)
|
||||
StatusEditBar(noteStatus?.note?.event?.content, it.address, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-14
@@ -38,7 +38,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -48,7 +47,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.map
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
@@ -56,6 +54,7 @@ import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName
|
||||
import com.vitorpamplona.amethyst.ui.screen.AroundMeFeedDefinition
|
||||
@@ -251,11 +250,7 @@ fun RenderOption(option: Name) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
val noteState by
|
||||
option.note
|
||||
.live()
|
||||
.metadata
|
||||
.observeAsState()
|
||||
val noteState by observeNote(option.note)
|
||||
|
||||
val name = (noteState?.note?.event as? PeopleListEvent)?.nameOrTitle() ?: option.note.dTag() ?: ""
|
||||
|
||||
@@ -267,14 +262,9 @@ fun RenderOption(option: Name) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
val name by
|
||||
option.note
|
||||
.live()
|
||||
.metadata
|
||||
.map { "/n/" + ((it.note as? AddressableNote)?.dTag() ?: "") }
|
||||
.observeAsState()
|
||||
val it by observeNote(option.note)
|
||||
|
||||
Text(text = name ?: "", color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(text = "/n/${((it?.note as? AddressableNote)?.dTag() ?: "")}", color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache.users
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
|
||||
@@ -36,13 +36,13 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
@@ -61,10 +61,7 @@ fun BadgeCompose(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by likeSetCard.note
|
||||
.live()
|
||||
.metadata
|
||||
.observeAsState()
|
||||
val noteState by observeNote(likeSetCard.note)
|
||||
val note = noteState?.note
|
||||
|
||||
val context = LocalContext.current.applicationContext
|
||||
|
||||
@@ -43,7 +43,6 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -62,8 +61,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import com.vitorpamplona.amethyst.R
|
||||
@@ -72,6 +69,10 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
|
||||
@@ -302,28 +303,16 @@ fun RenderClassifiedsThumb(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? ClassifiedsEvent ?: return
|
||||
if (baseNote.event !is ClassifiedsEvent) return
|
||||
|
||||
val card by
|
||||
baseNote
|
||||
.live()
|
||||
.metadata
|
||||
.map {
|
||||
val noteEvent = it.note.event as? ClassifiedsEvent
|
||||
|
||||
ClassifiedsThumb(
|
||||
image = noteEvent?.image(),
|
||||
title = noteEvent?.title(),
|
||||
price = noteEvent?.price(),
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
ClassifiedsThumb(
|
||||
image = noteEvent.image(),
|
||||
title = noteEvent.title(),
|
||||
price = noteEvent.price(),
|
||||
),
|
||||
)
|
||||
val card by observeNoteAndMap(baseNote) {
|
||||
val noteEvent = it.event as? ClassifiedsEvent
|
||||
ClassifiedsThumb(
|
||||
image = noteEvent?.image(),
|
||||
title = noteEvent?.title(),
|
||||
price = noteEvent?.price(),
|
||||
)
|
||||
}
|
||||
|
||||
InnerRenderClassifiedsThumb(card, baseNote)
|
||||
}
|
||||
@@ -425,39 +414,36 @@ fun RenderLiveActivityThumb(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return
|
||||
val card by observeNoteAndMap(baseNote) {
|
||||
val noteEvent = it.event as? LiveActivitiesEvent
|
||||
|
||||
val card by
|
||||
baseNote
|
||||
.live()
|
||||
.metadata
|
||||
.map {
|
||||
val noteEvent = it.note.event as? LiveActivitiesEvent
|
||||
LiveActivityCard(
|
||||
name = noteEvent?.dTag() ?: "",
|
||||
cover = noteEvent?.image()?.ifBlank { null },
|
||||
media = noteEvent?.streaming(),
|
||||
subject = noteEvent?.title()?.ifBlank { null },
|
||||
content = noteEvent?.summary(),
|
||||
participants = noteEvent?.participants()?.toImmutableList() ?: persistentListOf(),
|
||||
status = noteEvent?.status(),
|
||||
starts = noteEvent?.starts(),
|
||||
)
|
||||
}
|
||||
|
||||
LiveActivityCard(
|
||||
name = noteEvent?.dTag() ?: "",
|
||||
cover = noteEvent?.image()?.ifBlank { null },
|
||||
media = noteEvent?.streaming(),
|
||||
subject = noteEvent?.title()?.ifBlank { null },
|
||||
content = noteEvent?.summary(),
|
||||
participants = noteEvent?.participants()?.toImmutableList() ?: persistentListOf(),
|
||||
status = noteEvent?.status(),
|
||||
starts = noteEvent?.starts(),
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
LiveActivityCard(
|
||||
name = noteEvent.dTag(),
|
||||
cover = noteEvent.image()?.ifBlank { null },
|
||||
media = noteEvent.streaming(),
|
||||
subject = noteEvent.title()?.ifBlank { null },
|
||||
content = noteEvent.summary(),
|
||||
participants = noteEvent.participants().toImmutableList(),
|
||||
status = noteEvent.status(),
|
||||
starts = noteEvent.starts(),
|
||||
),
|
||||
)
|
||||
RenderLiveActivityThumb(
|
||||
card,
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderLiveActivityThumb(
|
||||
card: LiveActivityCard,
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
@@ -560,31 +546,29 @@ fun RenderCommunitiesThumb(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? CommunityDefinitionEvent ?: return
|
||||
val noteState by observeNote(baseNote)
|
||||
val noteEvent = noteState?.note?.event as? CommunityDefinitionEvent ?: return
|
||||
|
||||
val card by
|
||||
baseNote
|
||||
.live()
|
||||
.metadata
|
||||
.map {
|
||||
val noteEvent = it.note.event as? CommunityDefinitionEvent
|
||||
|
||||
CommunityCard(
|
||||
name = noteEvent?.dTag() ?: "",
|
||||
description = noteEvent?.description(),
|
||||
cover = noteEvent?.image()?.imageUrl,
|
||||
moderators = noteEvent?.moderatorKeys()?.toImmutableList() ?: persistentListOf(),
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
CommunityCard(
|
||||
name = noteEvent.dTag(),
|
||||
description = noteEvent.description(),
|
||||
cover = noteEvent.image()?.imageUrl,
|
||||
moderators = noteEvent.moderatorKeys().toImmutableList(),
|
||||
),
|
||||
)
|
||||
RenderCommunitiesThumb(
|
||||
CommunityCard(
|
||||
name = noteEvent.dTag(),
|
||||
description = noteEvent.description(),
|
||||
cover = noteEvent.image()?.imageUrl,
|
||||
moderators = noteEvent.moderatorKeys().toImmutableList(),
|
||||
),
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderCommunitiesThumb(
|
||||
card: CommunityCard,
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
LeftPictureLayout(
|
||||
onImage = {
|
||||
card.cover?.let {
|
||||
@@ -767,12 +751,7 @@ fun RenderContentDVMThumb(
|
||||
nav: INav,
|
||||
) {
|
||||
// downloads user metadata to pre-load the NIP-65 relays.
|
||||
val user =
|
||||
baseNote.author
|
||||
?.live()
|
||||
?.metadata
|
||||
?.observeAsState()
|
||||
|
||||
baseNote.author?.let { UserFinderFilterAssemblerSubscription(it) }
|
||||
val card = observeAppDefinition(appDefinitionNote = baseNote)
|
||||
|
||||
LeftPictureLayout(
|
||||
@@ -791,7 +770,7 @@ fun RenderContentDVMThumb(
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
user?.value?.user?.let {
|
||||
baseNote.author?.let {
|
||||
BannerImage(
|
||||
it,
|
||||
Modifier
|
||||
@@ -930,7 +909,7 @@ fun RenderChannelThumb(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val channelUpdates by channel.live.observeAsState()
|
||||
val channelUpdates by observeChannel(channel)
|
||||
|
||||
val name = remember(channelUpdates) { channelUpdates?.channel?.toBestDisplayName() ?: "" }
|
||||
val description = remember(channelUpdates) { channelUpdates?.channel?.summary()?.ifBlank { null } }
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
@@ -162,7 +163,7 @@ fun LoadOts(
|
||||
) {
|
||||
var earliestDate: GenericLoadable<Long> by remember { mutableStateOf(GenericLoadable.Loading()) }
|
||||
|
||||
val noteStatus by note.live().innerOts.observeAsState()
|
||||
val noteStatus by observeNoteOts(note)
|
||||
|
||||
LaunchedEffect(key1 = noteStatus) {
|
||||
accountViewModel.findOtsEventsForNote(noteStatus?.note ?: note) { newOts ->
|
||||
|
||||
@@ -43,7 +43,6 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -69,6 +68,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.ui.components.CoreSecretMessage
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
@@ -623,7 +623,7 @@ private fun WatchUserMetadata(
|
||||
author: User,
|
||||
onNewMetadata: @Composable (String?) -> Unit,
|
||||
) {
|
||||
val userProfile by author.live().profilePictureChanges.observeAsState(author.profilePicture())
|
||||
val userProfile by observeUserPicture(author)
|
||||
|
||||
onNewMetadata(userProfile)
|
||||
}
|
||||
|
||||
+8
-7
@@ -35,7 +35,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -52,6 +51,9 @@ import com.vitorpamplona.amethyst.commons.hashtags.Tunestr
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNip05
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
@@ -133,7 +135,7 @@ fun ObserveDisplayNip05Status(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val nip05 by baseUser.live().nip05Changes.observeAsState(baseUser.nip05())
|
||||
val nip05 by observeUserNip05(baseUser)
|
||||
|
||||
LoadStatuses(baseUser, accountViewModel) { statuses ->
|
||||
CrossfadeIfEnabled(
|
||||
@@ -204,10 +206,9 @@ fun ObserveRotateStatuses(
|
||||
|
||||
@Composable
|
||||
fun ObserveAllStatusesToAvoidSwitchigAllTheTime(statuses: ImmutableList<AddressableNote>) {
|
||||
statuses
|
||||
.map {
|
||||
it.live().metadata.observeAsState()
|
||||
}
|
||||
statuses.map {
|
||||
EventFinderFilterAssemblerSubscription(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -247,7 +248,7 @@ fun DisplayStatus(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by addressableNote.live().metadata.observeAsState()
|
||||
val noteState by observeNote(addressableNote)
|
||||
val noteEvent = noteState?.note?.event as? StatusEvent ?: return
|
||||
|
||||
DisplayStatus(noteEvent, accountViewModel, nav)
|
||||
|
||||
@@ -52,12 +52,16 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblerSubscription
|
||||
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
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
@@ -849,14 +853,14 @@ fun ObserveDraftEvent(
|
||||
accountViewModel: AccountViewModel,
|
||||
render: @Composable (Note) -> Unit,
|
||||
) {
|
||||
val noteState by note.live().metadata.observeAsState()
|
||||
val noteEvent by observeNoteEvent<DraftEvent>(note)
|
||||
|
||||
val noteEvent = noteState?.note?.event as? DraftEvent ?: return
|
||||
noteEvent?.let {
|
||||
val innerNote by produceCachedStateAsync(cache = accountViewModel.draftNoteCache, key = it)
|
||||
|
||||
val innerNote = produceCachedStateAsync(cache = accountViewModel.draftNoteCache, key = noteEvent)
|
||||
|
||||
innerNote.value?.let {
|
||||
render(it)
|
||||
innerNote?.let {
|
||||
render(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,7 +1113,7 @@ fun observeEdits(
|
||||
)
|
||||
}
|
||||
|
||||
val updatedNote by baseNote.live().innerModifications.observeAsState()
|
||||
val updatedNote by observeNoteEdits(baseNote)
|
||||
|
||||
LaunchedEffect(key1 = updatedNote) {
|
||||
updatedNote?.note?.let {
|
||||
@@ -1186,6 +1190,8 @@ private fun ChannelNotePicture(
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
) {
|
||||
ChannelFinderFilterAssemblerSubscription(baseChannel, Amethyst.instance.sources.channelFinder)
|
||||
|
||||
val model by
|
||||
baseChannel.live
|
||||
.map { it.channel.profilePicture() }
|
||||
|
||||
@@ -90,14 +90,13 @@ import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.WarningColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.isLight
|
||||
import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNAddr
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -51,7 +51,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -75,6 +74,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.StringToastMsg
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
@@ -283,7 +283,7 @@ private fun WatchZapsAndUpdateTallies(
|
||||
baseNote: Note,
|
||||
pollViewModel: PollNoteViewModel,
|
||||
) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
val zapsState by observeNoteZaps(baseNote)
|
||||
|
||||
LaunchedEffect(key1 = zapsState) { pollViewModel.refreshTallies() }
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
@@ -97,15 +96,20 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReactionCount
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReactions
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReferences
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplyCount
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteRepostCount
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReposts
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteRepostsBy
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
@@ -344,7 +348,7 @@ fun RenderZapRaiser(
|
||||
details: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
val zapsState by observeNoteZaps(baseNote)
|
||||
|
||||
var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) }
|
||||
|
||||
@@ -394,15 +398,7 @@ private fun WatchReactionsZapsBoostsAndDisplayIfExists(
|
||||
baseNote: Note,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val hasReactions by
|
||||
baseNote
|
||||
.live()
|
||||
.hasReactions
|
||||
.observeAsState(
|
||||
baseNote.zaps.isNotEmpty() ||
|
||||
baseNote.boosts.isNotEmpty() ||
|
||||
baseNote.reactions.isNotEmpty(),
|
||||
)
|
||||
val hasReactions by observeNoteReferences(baseNote)
|
||||
|
||||
if (hasReactions) {
|
||||
content()
|
||||
@@ -463,15 +459,7 @@ private fun ReactionDetailGallery(
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
|
||||
val hasReactions by
|
||||
baseNote
|
||||
.live()
|
||||
.hasReactions
|
||||
.observeAsState(
|
||||
baseNote.zaps.isNotEmpty() ||
|
||||
baseNote.boosts.isNotEmpty() ||
|
||||
baseNote.reactions.isNotEmpty(),
|
||||
)
|
||||
val hasReactions by observeNoteReferences(baseNote)
|
||||
|
||||
if (hasReactions) {
|
||||
Row(
|
||||
@@ -480,8 +468,8 @@ private fun ReactionDetailGallery(
|
||||
) {
|
||||
Column {
|
||||
WatchZapAndRenderGallery(baseNote, backgroundColor, nav, accountViewModel)
|
||||
WatchBoostsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
WatchReactionsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
// WatchBoostsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
// WatchReactionsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,7 +481,7 @@ private fun WatchBoostsAndRenderGallery(
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val boostsEvents by baseNote.live().boosts.observeAsState()
|
||||
val boostsEvents by observeNoteReposts(baseNote)
|
||||
|
||||
boostsEvents?.let {
|
||||
if (it.note.boosts.isNotEmpty()) {
|
||||
@@ -512,7 +500,7 @@ private fun WatchReactionsAndRenderGallery(
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
val reactionsState by observeNoteReactions(baseNote)
|
||||
val reactionEvents = reactionsState?.note?.reactions ?: return
|
||||
|
||||
if (reactionEvents.isNotEmpty()) {
|
||||
@@ -535,7 +523,7 @@ private fun WatchZapAndRenderGallery(
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
val zapsState by observeNoteZaps(baseNote)
|
||||
|
||||
var zapEvents by
|
||||
remember(zapsState) {
|
||||
@@ -692,7 +680,7 @@ fun ReplyCounter(
|
||||
textColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val repliesState by baseNote.live().replyCount.observeAsState(baseNote.replies.size)
|
||||
val repliesState by observeNoteReplyCount(baseNote)
|
||||
|
||||
SlidingAnimationCount(repliesState, textColor, accountViewModel)
|
||||
}
|
||||
@@ -827,16 +815,7 @@ fun ObserveBoostIcon(
|
||||
accountViewModel: AccountViewModel,
|
||||
inner: @Composable (Boolean) -> Unit,
|
||||
) {
|
||||
val hasBoosted by
|
||||
remember(baseNote) {
|
||||
baseNote
|
||||
.live()
|
||||
.boosts
|
||||
.map { it.note.isBoostedBy(accountViewModel.userProfile()) }
|
||||
.distinctUntilChanged()
|
||||
}.observeAsState(
|
||||
baseNote.isBoostedBy(accountViewModel.userProfile()),
|
||||
)
|
||||
val hasBoosted by observeNoteRepostsBy(baseNote, accountViewModel.userProfile())
|
||||
|
||||
inner(hasBoosted)
|
||||
}
|
||||
@@ -847,7 +826,7 @@ fun BoostText(
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val boostState by baseNote.live().boostCount.observeAsState(baseNote.boosts.size)
|
||||
val boostState by observeNoteRepostCount(baseNote)
|
||||
|
||||
SlidingAnimationCount(boostState, grayTint, accountViewModel)
|
||||
}
|
||||
@@ -917,7 +896,7 @@ fun ObserveLikeIcon(
|
||||
accountViewModel: AccountViewModel,
|
||||
inner: @Composable (String?) -> Unit,
|
||||
) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
val reactionsState by observeNoteReactions(baseNote)
|
||||
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
val reactionType by
|
||||
@@ -974,7 +953,7 @@ fun ObserveLikeText(
|
||||
baseNote: Note,
|
||||
inner: @Composable (Int) -> Unit,
|
||||
) {
|
||||
val reactionCount by baseNote.live().reactionCount.observeAsState(0)
|
||||
val reactionCount by observeNoteReactionCount(baseNote)
|
||||
|
||||
inner(reactionCount)
|
||||
}
|
||||
@@ -1227,7 +1206,7 @@ fun ObserveZapIcon(
|
||||
val wasZappedByLoggedInUser = remember { mutableStateOf(false) }
|
||||
|
||||
if (!wasZappedByLoggedInUser.value) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
val zapsState by observeNoteZaps(baseNote)
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
if (zapsState?.note?.zapPayments?.isNotEmpty() == true || zapsState?.note?.zaps?.isNotEmpty() == true) {
|
||||
@@ -1249,7 +1228,7 @@ fun ObserveZapAmountText(
|
||||
accountViewModel: AccountViewModel,
|
||||
inner: @Composable (String) -> Unit,
|
||||
) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
val zapsState by observeNoteZaps(baseNote)
|
||||
|
||||
if (zapsState?.note?.zapPayments?.isNotEmpty() == true) {
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
|
||||
@@ -29,7 +29,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -37,6 +36,7 @@ import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
@@ -120,7 +120,7 @@ private fun ReplyInfoMention(
|
||||
prefix: String,
|
||||
onUserTagClick: (User) -> Unit,
|
||||
) {
|
||||
val innerUserState by user.live().userMetadataInfo.observeAsState()
|
||||
val innerUserState by observeUserInfo(user)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = "$prefix${innerUserState?.bestName()}",
|
||||
|
||||
+4
-14
@@ -55,7 +55,6 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -72,8 +71,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
@@ -81,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap
|
||||
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||
@@ -387,17 +385,9 @@ private fun EmojiSelector(
|
||||
accountViewModel,
|
||||
) { emptyNote ->
|
||||
emptyNote?.let { usersEmojiList ->
|
||||
val collections by
|
||||
usersEmojiList
|
||||
.live()
|
||||
.metadata
|
||||
.map { (it.note.event as? EmojiPackSelectionEvent)?.emojiPackIds()?.toImmutableList() }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
(usersEmojiList.event as? EmojiPackSelectionEvent)
|
||||
?.emojiPackIds()
|
||||
?.toImmutableList(),
|
||||
)
|
||||
val collections by observeNoteEventAndMap(usersEmojiList) { event: EmojiPackSelectionEvent ->
|
||||
event.emojiPackIds().toImmutableList()
|
||||
}
|
||||
|
||||
collections?.let { EmojiCollectionGallery(it, accountViewModel, nav, onClick) }
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
|
||||
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -40,6 +39,7 @@ import com.vitorpamplona.amethyst.R
|
||||
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.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
@@ -328,7 +328,7 @@ fun LoadUserProfilePicture(
|
||||
baseUser: User,
|
||||
innerContent: @Composable (String?, String?) -> Unit,
|
||||
) {
|
||||
val userProfile by baseUser.live().userMetadataInfo.observeAsState(baseUser.info)
|
||||
val userProfile by observeUserInfo(baseUser)
|
||||
|
||||
innerContent(userProfile?.profilePicture(), userProfile?.bestName())
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -38,6 +37,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
@@ -67,8 +68,7 @@ fun WatchAuthor(
|
||||
if (noteAuthor != null) {
|
||||
inner(noteAuthor)
|
||||
} else {
|
||||
val authorState by baseNote.live().metadata.observeAsState()
|
||||
|
||||
val authorState by observeNote(baseNote)
|
||||
authorState?.note?.author?.let {
|
||||
inner(it)
|
||||
}
|
||||
@@ -86,8 +86,7 @@ fun WatchAuthorWithBlank(
|
||||
if (noteAuthor != null) {
|
||||
inner(noteAuthor)
|
||||
} else {
|
||||
val authorState by baseNote.live().metadata.observeAsState()
|
||||
|
||||
val authorState by observeNote(baseNote)
|
||||
CrossfadeIfEnabled(targetState = authorState?.note?.author, modifier = modifier, label = "WatchAuthorWithBlank", accountViewModel = accountViewModel) { newAuthor ->
|
||||
inner(newAuthor)
|
||||
}
|
||||
@@ -102,7 +101,7 @@ fun UsernameDisplay(
|
||||
textColor: Color = Color.Unspecified,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val userMetadata by baseUser.live().userMetadataInfo.observeAsState(baseUser.info)
|
||||
val userMetadata by observeUserInfo(baseUser)
|
||||
|
||||
CrossfadeIfEnabled(targetState = userMetadata, modifier = weight, label = "UsernameDisplay", accountViewModel = accountViewModel) {
|
||||
val name = it?.bestName()
|
||||
|
||||
@@ -24,10 +24,10 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -74,8 +74,7 @@ fun WatchNoteEvent(
|
||||
onNoteEventFound()
|
||||
} else {
|
||||
// avoid observing costs if already has an event.
|
||||
|
||||
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
|
||||
val hasEvent by observeNoteHasEvent(baseNote)
|
||||
CrossfadeIfEnabled(targetState = hasEvent, label = "Event presence", accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
onNoteEventFound()
|
||||
|
||||
@@ -29,7 +29,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -44,6 +43,8 @@ import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -65,10 +66,7 @@ fun ZapNoteCompose(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val baseNoteRequest by baseReqResponse.zapRequest
|
||||
.live()
|
||||
.metadata
|
||||
.observeAsState()
|
||||
val baseNoteRequest by observeNote(baseReqResponse.zapRequest)
|
||||
|
||||
var baseAuthor by remember { mutableStateOf<User?>(null) }
|
||||
|
||||
@@ -135,7 +133,7 @@ private fun RenderZapNote(
|
||||
|
||||
@Composable
|
||||
private fun ZapAmount(zapEventNote: Note) {
|
||||
val noteState by zapEventNote.live().metadata.observeAsState()
|
||||
val noteState by observeNote(zapEventNote)
|
||||
|
||||
var zapAmount by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
@@ -221,12 +219,10 @@ fun ShowFollowingOrUnfollowingButton(
|
||||
|
||||
@Composable
|
||||
fun AboutDisplay(baseAuthor: User) {
|
||||
val baseAuthorState by baseAuthor.live().metadata.observeAsState()
|
||||
val userAboutMe by
|
||||
remember(baseAuthorState) { derivedStateOf { baseAuthorState?.user?.info?.about ?: "" } }
|
||||
val aboutMe by observeUserAboutMe(baseAuthor)
|
||||
|
||||
Text(
|
||||
userAboutMe,
|
||||
aboutMe,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
+10
-19
@@ -22,9 +22,8 @@ package com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
|
||||
@@ -38,23 +37,15 @@ fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) {
|
||||
accountViewModel,
|
||||
) { emptyNote ->
|
||||
emptyNote?.let { usersEmojiList ->
|
||||
val collections by usersEmojiList
|
||||
.live()
|
||||
.metadata
|
||||
.map {
|
||||
(it.note.event as? EmojiPackSelectionEvent)
|
||||
?.taggedAddresses()
|
||||
?.toImmutableList()
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
(usersEmojiList.event as? EmojiPackSelectionEvent)
|
||||
?.taggedAddresses()
|
||||
?.toImmutableList(),
|
||||
)
|
||||
val collections by observeNoteEventAndMap(usersEmojiList) { event: EmojiPackSelectionEvent ->
|
||||
event.taggedAddresses().toImmutableList()
|
||||
}
|
||||
|
||||
collections?.forEach {
|
||||
LoadAddressableNote(it, accountViewModel) {
|
||||
it?.live()?.metadata?.observeAsState()
|
||||
collections?.forEach { address ->
|
||||
LoadAddressableNote(address, accountViewModel) { note ->
|
||||
if (note != null) {
|
||||
EventFinderFilterAssemblerSubscription(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
-3
@@ -28,15 +28,20 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.note.AboutDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
@@ -50,14 +55,50 @@ fun ShowUserSuggestionList(
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = Modifier.heightIn(0.dp, 200.dp),
|
||||
) {
|
||||
val userSuggestions by userSuggestions.results.collectAsStateWithLifecycle(emptyList())
|
||||
UserSearchDataSourceSubscription(userSuggestions)
|
||||
|
||||
if (userSuggestions.isNotEmpty()) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
AnimateOnNewSearch(userSuggestions, listState)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
userSuggestions.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
WatchResponses(userSuggestions, listState, onSelect, accountViewModel, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnimateOnNewSearch(
|
||||
userSuggestions: UserSuggestionState,
|
||||
listState: LazyListState,
|
||||
) {
|
||||
val searchTerm by userSuggestions.searchTerm.collectAsStateWithLifecycle("")
|
||||
|
||||
LaunchedEffect(searchTerm) {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchResponses(
|
||||
userSuggestions: UserSuggestionState,
|
||||
listState: LazyListState,
|
||||
onSelect: (User) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = Modifier.heightIn(0.dp, 200.dp),
|
||||
) {
|
||||
val suggestions by userSuggestions.results.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (suggestions.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(top = 10.dp),
|
||||
modifier = modifier,
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(userSuggestions, key = { _, item -> item.pubkeyHex }) { _, item ->
|
||||
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, item ->
|
||||
UserLine(item, accountViewModel) { onSelect(item) }
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
|
||||
+47
-17
@@ -22,43 +22,73 @@ package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions
|
||||
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class UserSuggestionState(
|
||||
val accountViewModel: AccountViewModel,
|
||||
) {
|
||||
var search = MutableStateFlow("")
|
||||
var results =
|
||||
search
|
||||
.debounce(500)
|
||||
val invalidations = MutableStateFlow<Int>(0)
|
||||
val currentWord = MutableStateFlow("")
|
||||
val searchDataSourceState = SearchQueryState(MutableStateFlow(""))
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val searchTerm =
|
||||
currentWord
|
||||
.debounce(300)
|
||||
.distinctUntilChanged()
|
||||
.map { word ->
|
||||
if (word.startsWith("@") && word.length > 2) {
|
||||
val prefix = word.removePrefix("@")
|
||||
NostrSearchEventOrUserDataSource.search(prefix)
|
||||
.map(::userSearchTermOrNull)
|
||||
.onEach(::updateDataSource)
|
||||
|
||||
val results =
|
||||
combine(searchTerm, invalidations.debounce(100)) { prefix, version ->
|
||||
if (prefix != null) {
|
||||
logTime("UserSuggestionState Search $prefix version $version") {
|
||||
accountViewModel.findUsersStartingWithSync(prefix)
|
||||
} else {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
search.tryEmit("")
|
||||
emptyList()
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
|
||||
fun reset() {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
search.tryEmit("")
|
||||
currentWord.tryEmit("")
|
||||
}
|
||||
|
||||
fun processCurrentWord(word: String) {
|
||||
search.tryEmit(word)
|
||||
currentWord.tryEmit(word)
|
||||
}
|
||||
|
||||
fun invalidateData() {
|
||||
// force new query
|
||||
invalidations.update { it + 1 }
|
||||
}
|
||||
|
||||
fun userSearchTermOrNull(currentWord: String): String? =
|
||||
if (currentWord.startsWith("@") && currentWord.length > 2) {
|
||||
currentWord.removePrefix("@")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun updateDataSource(searchTerm: String?) {
|
||||
if (searchTerm != null) {
|
||||
searchDataSourceState.searchQuery.tryEmit(searchTerm)
|
||||
} else {
|
||||
searchDataSourceState.searchQuery.tryEmit("")
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceCurrentWord(
|
||||
|
||||
+2
-3
@@ -29,7 +29,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -39,6 +38,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
@@ -106,8 +106,7 @@ fun ObserveRelayListForDMs(
|
||||
accountViewModel,
|
||||
) { relayList ->
|
||||
if (relayList != null) {
|
||||
val relayListNoteState by relayList.live().metadata.observeAsState()
|
||||
val relayListEvent = relayListNoteState?.note?.event as? ChatMessageRelayListEvent
|
||||
val relayListEvent by observeNoteEvent<ChatMessageRelayListEvent>(relayList)
|
||||
|
||||
inner(relayListEvent)
|
||||
}
|
||||
|
||||
+7
-6
@@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
@@ -36,6 +35,7 @@ import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBanner
|
||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchAuthor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -64,20 +64,21 @@ fun BannerImage(
|
||||
author: User,
|
||||
imageModifier: Modifier = Modifier.fillMaxWidth().heightIn(max = 200.dp),
|
||||
) {
|
||||
val currentInfo by author.live().userMetadataInfo.observeAsState()
|
||||
currentInfo?.banner?.let {
|
||||
val banner by observeUserBanner(author)
|
||||
|
||||
if (banner.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
model = banner,
|
||||
contentDescription =
|
||||
stringRes(
|
||||
R.string.preview_card_image_for,
|
||||
it,
|
||||
banner,
|
||||
),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = imageModifier,
|
||||
placeholder = painterResource(R.drawable.profile_banner),
|
||||
)
|
||||
} ?: run {
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.profile_banner),
|
||||
contentDescription = stringRes(R.string.profile_banner),
|
||||
|
||||
@@ -39,7 +39,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -58,6 +57,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplies
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableTextColor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
@@ -115,7 +115,7 @@ private fun RenderPledgeAmount(
|
||||
baseReward: Reward,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val repliesState by baseNote.live().replies.observeAsState()
|
||||
val repliesState by observeNoteReplies(baseNote)
|
||||
var reward by remember {
|
||||
mutableStateOf<String>(
|
||||
showAmount(baseReward.amount),
|
||||
|
||||
@@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||
|
||||
@@ -27,13 +27,15 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.appendLink
|
||||
@@ -80,7 +82,7 @@ fun ForkInformationRowLightColor(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by originalVersion.live().metadata.observeAsState()
|
||||
val noteState by observeNote(originalVersion)
|
||||
val note = noteState?.note ?: return
|
||||
val author = note.author ?: return
|
||||
val route = remember(note) { routeFor(note, accountViewModel.userProfile()) }
|
||||
@@ -103,20 +105,16 @@ fun ForkInformationRowLightColor(
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
|
||||
val userState by author.live().metadata.observeAsState()
|
||||
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
|
||||
val userTags =
|
||||
remember(userState) { userState?.user?.info?.tags }
|
||||
|
||||
if (userDisplayName != null) {
|
||||
val userState by observeUser(author)
|
||||
userState?.user?.toBestDisplayName()?.let {
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = userDisplayName,
|
||||
clickablePart = it,
|
||||
maxLines = 1,
|
||||
route = route,
|
||||
overrideColor = MaterialTheme.colorScheme.nip05,
|
||||
fontSize = Font14SP,
|
||||
nav = nav,
|
||||
tags = userTags,
|
||||
tags = userState?.user?.info?.tags,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -130,19 +128,19 @@ fun ForkInformationRow(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by originalVersion.live().metadata.observeAsState()
|
||||
val noteState by observeNote(originalVersion)
|
||||
val note = noteState?.note ?: return
|
||||
val route = remember(note) { routeFor(note, accountViewModel.userProfile()) }
|
||||
|
||||
if (route != null) {
|
||||
Row(modifier) {
|
||||
val author = note.author ?: return
|
||||
val meta by author.live().userMetadataInfo.observeAsState(author.info)
|
||||
val meta by observeUserInfo(author)
|
||||
|
||||
Text(stringRes(id = R.string.forked_from))
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
|
||||
val userMetadata by author.live().userMetadataInfo.observeAsState()
|
||||
val userMetadata by observeUserInfo(author)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = remember(meta) { meta?.bestName() ?: author.pubkeyDisplayHex() },
|
||||
|
||||
+2
-3
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.elements
|
||||
|
||||
import android.R.attr.onClick
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -40,7 +39,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -65,6 +63,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.appendLink
|
||||
@@ -189,7 +188,7 @@ fun ZapTheDevsCard(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val releaseNoteState by baseNote.live().metadata.observeAsState()
|
||||
val releaseNoteState by observeNote(baseNote)
|
||||
val releaseNote = releaseNoteState?.note ?: return
|
||||
|
||||
Row(modifier = Modifier.padding(start = Size10dp, end = Size10dp, bottom = Size10dp)) {
|
||||
|
||||
@@ -33,7 +33,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -47,6 +46,7 @@ import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -58,16 +58,17 @@ import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
|
||||
|
||||
@Composable
|
||||
fun BadgeDisplay(baseNote: Note) {
|
||||
val observingNote by baseNote.live().metadata.observeAsState()
|
||||
val badgeData = observingNote?.note?.event as? BadgeDefinitionEvent ?: return
|
||||
val badgeData by observeNoteEvent<BadgeDefinitionEvent>(baseNote)
|
||||
|
||||
RenderBadge(
|
||||
badgeData.image(),
|
||||
badgeData.name(),
|
||||
MaterialTheme.colorScheme.background,
|
||||
MaterialTheme.colorScheme.onBackground,
|
||||
badgeData.description(),
|
||||
)
|
||||
badgeData?.let {
|
||||
RenderBadge(
|
||||
it.image(),
|
||||
it.name(),
|
||||
MaterialTheme.colorScheme.background,
|
||||
MaterialTheme.colorScheme.onBackground,
|
||||
it.description(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
||||
+19
-21
@@ -35,7 +35,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -52,6 +51,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
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.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
@@ -119,19 +119,17 @@ fun LongCommunityHeader(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val noteEvent =
|
||||
remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
|
||||
val noteEvent by observeNoteEvent<CommunityDefinitionEvent>(baseNote)
|
||||
|
||||
Row(
|
||||
lineModifier,
|
||||
) {
|
||||
val rulesLabel = stringRes(id = R.string.rules)
|
||||
val summary =
|
||||
remember(noteState) {
|
||||
val subject = noteEvent.subject()?.ifEmpty { null }
|
||||
val body = noteEvent.description()?.ifBlank { null }
|
||||
val rules = noteEvent.rules()?.ifBlank { null }
|
||||
remember(noteEvent) {
|
||||
val subject = noteEvent?.subject()?.ifEmpty { null }
|
||||
val body = noteEvent?.description()?.ifBlank { null }
|
||||
val rules = noteEvent?.rules()?.ifBlank { null }
|
||||
|
||||
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
|
||||
if (rules == null) {
|
||||
@@ -167,12 +165,14 @@ fun LongCommunityHeader(
|
||||
)
|
||||
}
|
||||
|
||||
if (summary != null && noteEvent.hasHashtags()) {
|
||||
DisplayUncitedHashtags(
|
||||
event = noteEvent,
|
||||
content = summary,
|
||||
nav = nav,
|
||||
)
|
||||
noteEvent?.let {
|
||||
if (it.hasHashtags()) {
|
||||
DisplayUncitedHashtags(
|
||||
event = it,
|
||||
content = summary ?: "",
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,8 +207,8 @@ fun LongCommunityHeader(
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = noteState) {
|
||||
val participants = (noteState?.note?.event as? CommunityDefinitionEvent)?.moderators()
|
||||
LaunchedEffect(key1 = noteEvent) {
|
||||
val participants = noteEvent?.moderators()
|
||||
|
||||
if (participants != null) {
|
||||
accountViewModel.loadParticipants(participants) { newParticipantUsers ->
|
||||
@@ -263,12 +263,10 @@ fun ShortCommunityHeader(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val noteEvent =
|
||||
remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
|
||||
val noteEvent by observeNoteEvent<CommunityDefinitionEvent>(baseNote)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
noteEvent.image()?.let {
|
||||
noteEvent?.image()?.let {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = baseNote.idHex,
|
||||
model = it.imageUrl,
|
||||
@@ -290,7 +288,7 @@ fun ShortCommunityHeader(
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = remember(noteState) { noteEvent.dTag() },
|
||||
text = noteEvent?.name() ?: baseNote.dTag(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -46,10 +45,10 @@ 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.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
@@ -71,17 +70,9 @@ public fun RenderEmojiPack(
|
||||
accountViewModel: AccountViewModel,
|
||||
onClick: ((EmojiUrlTag) -> Unit)? = null,
|
||||
) {
|
||||
val noteEvent by
|
||||
baseNote
|
||||
.live()
|
||||
.metadata
|
||||
.map { it.note.event }
|
||||
.distinctUntilChanged()
|
||||
.observeAsState(baseNote.event)
|
||||
val noteEvent by observeNoteEvent<EmojiPackEvent>(baseNote)
|
||||
|
||||
if (noteEvent == null || noteEvent !is EmojiPackEvent) return
|
||||
|
||||
(noteEvent as? EmojiPackEvent)?.let {
|
||||
noteEvent?.let {
|
||||
RenderEmojiPack(
|
||||
noteEvent = it,
|
||||
baseNote = baseNote,
|
||||
@@ -188,14 +179,7 @@ private fun EmojiListOptions(
|
||||
accountViewModel,
|
||||
) {
|
||||
it?.let { usersEmojiList ->
|
||||
val hasAddedThis by
|
||||
remember {
|
||||
usersEmojiList
|
||||
.live()
|
||||
.metadata
|
||||
.map { usersEmojiList.event?.isTaggedAddressableNote(emojiPackNote.idHex) }
|
||||
.distinctUntilChanged()
|
||||
}.observeAsState()
|
||||
val hasAddedThis by observeNoteAndMap(usersEmojiList) { usersEmojiList.event?.isTaggedAddressableNote(emojiPackNote.idHex) }
|
||||
|
||||
CrossfadeIfEnabled(targetState = hasAddedThis, label = "EmojiListOptions", accountViewModel = accountViewModel) {
|
||||
if (it != true) {
|
||||
|
||||
@@ -22,16 +22,15 @@ package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
@@ -68,9 +67,7 @@ private fun ObserverAndRenderNIP95(
|
||||
) {
|
||||
val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return
|
||||
|
||||
val appContext = LocalContext.current.applicationContext
|
||||
|
||||
val noteState by content.live().metadata.observeAsState()
|
||||
val noteState by observeNote(content)
|
||||
|
||||
val content by
|
||||
remember(noteState) {
|
||||
|
||||
@@ -33,7 +33,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -43,6 +42,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
@@ -100,13 +100,12 @@ private fun RenderShortRepositoryHeader(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val noteEvent = noteState?.note?.event as? GitRepositoryEvent ?: return
|
||||
val noteEvent by observeNoteEvent<GitRepositoryEvent>(baseNote)
|
||||
|
||||
Column(
|
||||
modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp),
|
||||
) {
|
||||
val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() }
|
||||
val title = noteEvent?.name() ?: baseNote.dTag()
|
||||
Text(
|
||||
text = stringRes(id = R.string.git_repository, title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
@@ -115,7 +114,7 @@ private fun RenderShortRepositoryHeader(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
noteEvent.description()?.let {
|
||||
noteEvent?.description()?.let {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Text(
|
||||
text = it,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user