mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 01:07:46 +00:00
Moves NIP-55 calls to be suspending functions.
Moves NIP-55 calls to include an ID per call, not per event. Adds error handling facilities to the Signer functions. Moves the indexing of the decrypted objects to outside the LocalCache Migrates Signers to become suspending functions. Migrates Decryption caching systems to outside the Events themselves. Migrates all NIP-51 lists to the new structure. Migrates Drafts and NIP-04 and NIP-17 DMs to the new structure Migrates Bookmarks to the new structure. Changes the Room route to avoid using hashcode.
This commit is contained in:
@@ -24,8 +24,9 @@ import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
|
||||
import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor
|
||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.uploads.ImageDownloader
|
||||
@@ -38,6 +39,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.fail
|
||||
@@ -49,6 +51,7 @@ import okhttp3.OkHttpClient
|
||||
import org.junit.Assert
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.Request.method
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.random.Random
|
||||
@@ -57,12 +60,16 @@ import kotlin.random.Random
|
||||
class ImageUploadTesting {
|
||||
companion object {
|
||||
val accountSettings = AccountSettings(KeyPair())
|
||||
val signer = NostrSignerInternal(accountSettings.keyPair)
|
||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
val cache = LocalCache
|
||||
|
||||
val account =
|
||||
Account(
|
||||
val blossomServerListState =
|
||||
BlossomServerListState(
|
||||
signer = signer,
|
||||
cache = cache,
|
||||
scope = scope,
|
||||
settings = accountSettings,
|
||||
signer = NostrSignerInternal(accountSettings.keyPair),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -111,7 +118,7 @@ class ImageUploadTesting {
|
||||
sensitiveContent = null,
|
||||
serverBaseUrl = server.baseUrl,
|
||||
okHttpClient = { client },
|
||||
httpAuth = account::createBlossomUploadAuth,
|
||||
httpAuth = blossomServerListState::createBlossomUploadAuth,
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
)
|
||||
|
||||
@@ -139,25 +146,27 @@ class ImageUploadTesting {
|
||||
{ client },
|
||||
)
|
||||
|
||||
val paylod = getBitmap()
|
||||
val inputStream = paylod.inputStream()
|
||||
val payload = getBitmap()
|
||||
val inputStream = payload.inputStream()
|
||||
val result =
|
||||
Nip96Uploader()
|
||||
.upload(
|
||||
inputStream = inputStream,
|
||||
length = paylod.size.toLong(),
|
||||
length = payload.size.toLong(),
|
||||
contentType = "image/png",
|
||||
alt = null,
|
||||
sensitiveContent = null,
|
||||
server = serverInfo,
|
||||
okHttpClient = { client },
|
||||
onProgress = {},
|
||||
httpAuth = account::createHTTPAuthorization,
|
||||
httpAuth = { url, method, body ->
|
||||
signer.sign(HTTPAuthorizationEvent.build(url, method, body))
|
||||
},
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
)
|
||||
|
||||
val url = result.url!!
|
||||
val size = result.size
|
||||
val size = result.size?.toInt()
|
||||
val dim = result.dimension
|
||||
val hash = result.sha256
|
||||
|
||||
@@ -183,7 +192,7 @@ class ImageUploadTesting {
|
||||
assertEquals("${server.name}: Invalid dimensions", it.dim.toString(), dim.toString())
|
||||
}
|
||||
if (size != null) {
|
||||
assertEquals("${server.name}: Invalid size", it.size.toString(), size)
|
||||
assertEquals("${server.name}: Invalid size", it.size, size)
|
||||
}
|
||||
},
|
||||
onFailure = { fail("${server.name}: It should not fail") },
|
||||
@@ -225,6 +234,7 @@ class ImageUploadTesting {
|
||||
testBase(ServerName("sove", "https://sove.rent", ServerType.NIP96))
|
||||
}
|
||||
|
||||
@Ignore("Not Working anymore")
|
||||
@Test()
|
||||
fun testNostrBuild() =
|
||||
runBlocking {
|
||||
@@ -239,6 +249,7 @@ class ImageUploadTesting {
|
||||
}
|
||||
|
||||
@Test()
|
||||
@Ignore("Not Working anymore")
|
||||
fun testVoidCat() =
|
||||
runBlocking {
|
||||
testBase(ServerName("void.cat", "https://void.cat", ServerType.NIP96))
|
||||
|
||||
@@ -30,12 +30,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class OkHttpOtsTest {
|
||||
@@ -49,9 +47,9 @@ class OkHttpOtsTest {
|
||||
val resolver =
|
||||
OtsResolver(
|
||||
OkHttpBitcoinExplorer(
|
||||
OkHttpBitcoinExplorer.MEMPOOL_API_URL,
|
||||
baseAPI = OkHttpBitcoinExplorer.MEMPOOL_API_URL,
|
||||
client = OkHttpClient.Builder().build(),
|
||||
otsCache,
|
||||
cache = otsCache,
|
||||
),
|
||||
OkHttpCalendarBuilder { OkHttpClient.Builder().build() },
|
||||
)
|
||||
@@ -80,20 +78,15 @@ class OkHttpOtsTest {
|
||||
@Test
|
||||
fun createOTSEventAndVerify() {
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
var ots: OtsEvent? = null
|
||||
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver)
|
||||
|
||||
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile)) {
|
||||
ots = it
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
val ots =
|
||||
runBlocking {
|
||||
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile))
|
||||
}
|
||||
|
||||
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
|
||||
println(ots!!.toJson())
|
||||
println(ots.toJson())
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
|
||||
// Should not be valid because we need to wait for confirmations
|
||||
|
||||
+100
-84
File diff suppressed because one or more lines are too long
@@ -43,10 +43,10 @@ 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.RelayProxyClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.RelaySpeedLogger
|
||||
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.relayClient.speedLogger.RelaySpeedLogger
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
@@ -118,7 +118,7 @@ class Amethyst : Application() {
|
||||
val notifyCoordinator = NotifyCoordinator(client)
|
||||
|
||||
// Authenticates with relays.
|
||||
val authCoordinator = AuthCoordinator(client)
|
||||
val authCoordinator = AuthCoordinator(client, applicationIOScope)
|
||||
|
||||
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -49,13 +48,14 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
@@ -324,7 +324,7 @@ object LocalPreferences {
|
||||
)
|
||||
putString(
|
||||
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
|
||||
JsonMapper.mapper.writeValueAsString(settings.zapPaymentRequest?.denormalize()),
|
||||
JsonMapper.mapper.writeValueAsString(settings.zapPaymentRequest.value?.denormalize()),
|
||||
)
|
||||
if (settings.backupContactList != null) {
|
||||
putString(
|
||||
@@ -522,7 +522,6 @@ object LocalPreferences {
|
||||
"Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}",
|
||||
e,
|
||||
)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -619,7 +618,7 @@ object LocalPreferences {
|
||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
||||
zapPaymentRequest = zapPaymentRequestServer?.normalize(),
|
||||
zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer?.normalize()),
|
||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
||||
@@ -663,7 +662,6 @@ object LocalPreferences {
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -678,7 +676,6 @@ object LocalPreferences {
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,26 +27,26 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
|
||||
@@ -66,9 +66,9 @@ import java.util.Locale
|
||||
val DefaultChannels =
|
||||
listOf(
|
||||
// Anigma's Nostr
|
||||
EventIdHint("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos),
|
||||
ChannelTag("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos),
|
||||
// Amethyst's Group
|
||||
EventIdHint("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos),
|
||||
ChannelTag("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos),
|
||||
)
|
||||
|
||||
val DefaultNIP65RelaySet = setOf(Constants.mom, Constants.nos, Constants.bitcoiner)
|
||||
@@ -115,7 +115,7 @@ class AccountSettings(
|
||||
val defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
val defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
val defaultDiscoveryFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
var zapPaymentRequest: Nip47WalletConnect.Nip47URINorm? = null,
|
||||
var zapPaymentRequest: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?> = MutableStateFlow(null),
|
||||
var hideDeleteRequestDialog: Boolean = false,
|
||||
var hideBlockAlertDialog: Boolean = false,
|
||||
var hideNIP17WarningDialog: Boolean = false,
|
||||
@@ -199,8 +199,8 @@ class AccountSettings(
|
||||
}
|
||||
|
||||
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean {
|
||||
if (zapPaymentRequest != newServer) {
|
||||
zapPaymentRequest = newServer
|
||||
if (zapPaymentRequest.value != newServer) {
|
||||
zapPaymentRequest.tryEmit(newServer)
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -138,11 +138,12 @@ class AccountLanguagePreferences(
|
||||
// language services
|
||||
// ---
|
||||
fun toggleDontTranslateFrom(languageCode: String) {
|
||||
if (!dontTranslateFrom.contains(languageCode)) {
|
||||
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
|
||||
} else {
|
||||
dontTranslateFrom = dontTranslateFrom.minus(languageCode)
|
||||
}
|
||||
dontTranslateFrom =
|
||||
if (!dontTranslateFrom.contains(languageCode)) {
|
||||
dontTranslateFrom.plus(languageCode)
|
||||
} else {
|
||||
dontTranslateFrom.minus(languageCode)
|
||||
}
|
||||
}
|
||||
|
||||
fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language)
|
||||
|
||||
@@ -168,7 +168,7 @@ abstract class Channel {
|
||||
var creator: User? = null
|
||||
var updatedMetadataAt: Long = 0
|
||||
val notes = LargeCache<HexKey, Note>()
|
||||
var lastNoteCreatedAt: Long = 0
|
||||
var lastNote: Note? = null
|
||||
|
||||
private var relays = mapOf<NormalizedRelayUrl, Counter>()
|
||||
|
||||
@@ -220,8 +220,8 @@ abstract class Channel {
|
||||
) {
|
||||
notes.put(note.idHex, note)
|
||||
|
||||
if ((note.createdAt() ?: 0) > lastNoteCreatedAt) {
|
||||
lastNoteCreatedAt = note.createdAt() ?: 0
|
||||
if ((note.createdAt() ?: 0) > (lastNote?.createdAt() ?: 0)) {
|
||||
lastNote = note
|
||||
}
|
||||
|
||||
if (relay != null) {
|
||||
|
||||
@@ -28,12 +28,12 @@ import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor
|
||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
|
||||
import com.vitorpamplona.amethyst.model.privateChats.ChatroomList
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.dateFormatter
|
||||
import com.vitorpamplona.ammolite.relays.BundledInsert
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
@@ -83,7 +83,6 @@ import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
@@ -120,6 +119,7 @@ import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore
|
||||
@@ -127,16 +127,16 @@ import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent
|
||||
@@ -178,12 +178,11 @@ import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
@@ -210,11 +209,12 @@ object LocalCache : ILocalCache {
|
||||
val notes = LargeCache<HexKey, Note>()
|
||||
val addressables = LargeCache<Address, AddressableNote>()
|
||||
|
||||
val chatroomList = LargeCache<HexKey, ChatroomList>()
|
||||
val publicChatChannels = LargeCache<HexKey, PublicChatChannel>()
|
||||
val liveChatChannels = LargeCache<Address, LiveActivitiesChannel>()
|
||||
val ephemeralChannels = LargeCache<RoomId, EphemeralChatChannel>()
|
||||
|
||||
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
|
||||
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, suspend (LnZapPaymentResponseEvent) -> Unit>>(10)
|
||||
|
||||
val relayHints = HintIndexer()
|
||||
|
||||
@@ -223,8 +223,6 @@ object LocalCache : ILocalCache {
|
||||
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,
|
||||
@@ -321,6 +319,8 @@ object LocalCache : ILocalCache {
|
||||
|
||||
fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId)
|
||||
|
||||
fun getChatroomListIfExists(key: String): ChatroomList? = chatroomList.get(key)
|
||||
|
||||
fun getPublicChatChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key)
|
||||
|
||||
fun getEphemeralChatChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key)
|
||||
@@ -400,20 +400,13 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreatePublicChatChannel(key: HexKey): PublicChatChannel =
|
||||
publicChatChannels.getOrCreate(key) {
|
||||
PublicChatChannel(key)
|
||||
}
|
||||
fun getOrCreateChatroomList(key: HexKey): ChatroomList = chatroomList.getOrCreate(key) { ChatroomList(key) }
|
||||
|
||||
fun getOrCreateLiveChannel(key: Address): LiveActivitiesChannel =
|
||||
liveChatChannels.getOrCreate(key) {
|
||||
LiveActivitiesChannel(key)
|
||||
}
|
||||
fun getOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = publicChatChannels.getOrCreate(key) { PublicChatChannel(key) }
|
||||
|
||||
fun getOrCreateEphemeralChannel(key: RoomId): EphemeralChatChannel =
|
||||
ephemeralChannels.getOrCreate(key) {
|
||||
EphemeralChatChannel(key)
|
||||
}
|
||||
fun getOrCreateLiveChannel(key: Address): LiveActivitiesChannel = liveChatChannels.getOrCreate(key) { LiveActivitiesChannel(key) }
|
||||
|
||||
fun getOrCreateEphemeralChannel(key: RoomId): EphemeralChatChannel = ephemeralChannels.getOrCreate(key) { EphemeralChatChannel(key) }
|
||||
|
||||
fun checkGetOrCreatePublicChatChannel(key: String): PublicChatChannel? {
|
||||
if (isValidHex(key)) {
|
||||
@@ -521,19 +514,7 @@ object LocalCache : ILocalCache {
|
||||
event: BookmarkListEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val user = getOrCreateUser(event.pubKey)
|
||||
if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) {
|
||||
if (event.dTag() == "bookmark") {
|
||||
if (wasVerified || justVerify(event)) {
|
||||
user.updateBookmark(event)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
fun formattedDateTime(timestamp: Long): String =
|
||||
Instant
|
||||
@@ -599,7 +580,7 @@ object LocalCache : ILocalCache {
|
||||
// Counts the replies
|
||||
replyTo.forEach { it.addReply(note) }
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
} else {
|
||||
@@ -711,7 +692,7 @@ object LocalCache : ILocalCache {
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -756,7 +737,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -864,7 +845,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
channel.updateChannelInfo(creator, event, event.createdAt)
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1054,7 +1035,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
author.flowSet?.statuses?.invalidateData()
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1085,7 +1066,7 @@ object LocalCache : ILocalCache {
|
||||
version.flowSet?.ots?.invalidateData()
|
||||
}
|
||||
|
||||
refreshObservers(version)
|
||||
refreshNewNoteObservers(version)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1175,7 +1156,7 @@ object LocalCache : ILocalCache {
|
||||
if (event.createdAt > (replaceableNote.createdAt() ?: 0) && (isVerified || justVerify(event))) {
|
||||
replaceableNote.loadEvent(event, author, computeReplyTo(event))
|
||||
|
||||
refreshObservers(replaceableNote)
|
||||
refreshNewNoteObservers(replaceableNote)
|
||||
|
||||
return true
|
||||
} else {
|
||||
@@ -1199,39 +1180,7 @@ object LocalCache : ILocalCache {
|
||||
event: PrivateDmEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return false
|
||||
|
||||
if (wasVerified || justVerify(event)) {
|
||||
val recipient = event.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }
|
||||
|
||||
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
||||
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
if (recipient != null) {
|
||||
author.addMessage(recipient, note)
|
||||
recipient.addMessage(author, note)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
) = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: DeletionEvent,
|
||||
@@ -1300,7 +1249,7 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
} else {
|
||||
@@ -1350,26 +1299,7 @@ object LocalCache : ILocalCache {
|
||||
getNoteIfExists(it)?.removeReply(deleteNote)
|
||||
}
|
||||
|
||||
if (deletedEvent is PrivateDmEvent) {
|
||||
val author = deleteNote.author
|
||||
val recipient =
|
||||
deletedEvent.verifiedRecipientPubKey()?.let {
|
||||
checkGetOrCreateUser(it)
|
||||
}
|
||||
|
||||
if (recipient != null && author != null) {
|
||||
author.removeMessage(recipient, deleteNote)
|
||||
recipient.removeMessage(author, deleteNote)
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedEvent is DraftEvent) {
|
||||
deletedEvent.allCache().forEach {
|
||||
it?.let {
|
||||
deindexDraftAsRealEvent(deleteNote, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
notes.remove(deleteNote.idHex)
|
||||
|
||||
if (deletedEvent is WrappedEvent) {
|
||||
deleteWraps(deletedEvent)
|
||||
@@ -1377,7 +1307,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
deleteNote.clearFlow()
|
||||
|
||||
notes.remove(deleteNote.idHex)
|
||||
refreshDeletedNoteObservers(deleteNote)
|
||||
}
|
||||
|
||||
fun deleteWraps(event: WrappedEvent) {
|
||||
@@ -1389,6 +1319,7 @@ object LocalCache : ILocalCache {
|
||||
deleteWraps(noteEvent)
|
||||
}
|
||||
it.clearFlow()
|
||||
refreshDeletedNoteObservers(it)
|
||||
}
|
||||
|
||||
notes.remove(it.id)
|
||||
@@ -1418,7 +1349,7 @@ object LocalCache : ILocalCache {
|
||||
justConsumeAndUpdateIndexes(it, relay, false)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1448,7 +1379,7 @@ object LocalCache : ILocalCache {
|
||||
justConsumeAndUpdateIndexes(it, relay, false)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1483,7 +1414,7 @@ object LocalCache : ILocalCache {
|
||||
justConsumeAndUpdateIndexes(it, relay, false)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1509,7 +1440,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
repliesTo.forEach { it.addReaction(note) }
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1553,7 +1484,7 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1576,7 +1507,7 @@ object LocalCache : ILocalCache {
|
||||
oldChannel.addNote(note, relay)
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
true
|
||||
} else {
|
||||
wasVerified
|
||||
@@ -1624,7 +1555,7 @@ object LocalCache : ILocalCache {
|
||||
oldChannel.addNote(note, relay)
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
}
|
||||
|
||||
return isVerified
|
||||
@@ -1709,7 +1640,6 @@ object LocalCache : ILocalCache {
|
||||
|
||||
if (wasVerified || justVerify(event)) {
|
||||
val existingZapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) }
|
||||
|
||||
if (existingZapRequest == null || existingZapRequest.event == null) {
|
||||
// tries to add it
|
||||
event.zapRequest?.let {
|
||||
@@ -1733,7 +1663,7 @@ object LocalCache : ILocalCache {
|
||||
repliesTo.forEach { it.addZap(zapRequest, note) }
|
||||
mentions.forEach { it.addZap(zapRequest, note) }
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1761,7 +1691,7 @@ object LocalCache : ILocalCache {
|
||||
repliesTo.forEach { it.addZap(note, null) }
|
||||
mentions.forEach { it.addZap(note, null) }
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1826,7 +1756,7 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1885,7 +1815,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
note.loadEvent(eventNoData, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1897,150 +1827,25 @@ object LocalCache : ILocalCache {
|
||||
event: ChatMessageEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return false
|
||||
|
||||
if (wasVerified || justVerify(event)) {
|
||||
val recipientsHex = event.groupMembers()
|
||||
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
|
||||
|
||||
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
||||
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
if (recipients.isNotEmpty()) {
|
||||
recipients.forEach {
|
||||
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
|
||||
|
||||
val authorGroup =
|
||||
if (groupMinusRecipient.isEmpty()) {
|
||||
// note to self
|
||||
ChatroomKey(persistentSetOf(it.pubkeyHex))
|
||||
} else {
|
||||
ChatroomKey(groupMinusRecipient.toImmutableSet())
|
||||
}
|
||||
|
||||
it.addMessage(authorGroup, note)
|
||||
}
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
) = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
private fun consume(
|
||||
event: ChatMessageEncryptedFileHeaderEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return false
|
||||
|
||||
if (wasVerified || justVerify(event)) {
|
||||
val recipientsHex = event.groupMembers()
|
||||
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
|
||||
|
||||
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
||||
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
if (recipients.isNotEmpty()) {
|
||||
recipients.forEach {
|
||||
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
|
||||
|
||||
val authorGroup =
|
||||
if (groupMinusRecipient.isEmpty()) {
|
||||
// note to self
|
||||
ChatroomKey(persistentSetOf(it.pubkeyHex))
|
||||
} else {
|
||||
ChatroomKey(groupMinusRecipient.toImmutableSet())
|
||||
}
|
||||
|
||||
it.addMessage(authorGroup, note)
|
||||
}
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
) = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: SealedRumorEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return false
|
||||
|
||||
if (wasVerified || justVerify(event)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
refreshObservers(note)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
) = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: GiftWrapEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null) {
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return false
|
||||
|
||||
if (wasVerified || justVerify(event)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
refreshObservers(note)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
) = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: LnZapPaymentRequestEvent,
|
||||
@@ -2056,7 +1861,7 @@ object LocalCache : ILocalCache {
|
||||
zappedNote: Note?,
|
||||
wasVerified: Boolean,
|
||||
relay: NormalizedRelayUrl?,
|
||||
onResponse: (LnZapPaymentResponseEvent) -> Unit,
|
||||
onResponse: suspend (LnZapPaymentResponseEvent) -> Unit,
|
||||
): Boolean {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
@@ -2075,7 +1880,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
awaitingPaymentRequests.put(event.id, Pair(zappedNote, onResponse))
|
||||
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -2106,7 +1911,9 @@ object LocalCache : ILocalCache {
|
||||
|
||||
requestNote?.let { request -> zappedNote?.addZapPayment(request, note) }
|
||||
|
||||
responseCallback(event)
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
responseCallback(event)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -2438,9 +2245,9 @@ object LocalCache : ILocalCache {
|
||||
pruneOldMessagesChannel(channel)
|
||||
}
|
||||
|
||||
users.forEach { _, user ->
|
||||
user.privateChatrooms.values.map {
|
||||
val toBeRemoved = it.pruneMessagesToTheLatestOnly()
|
||||
chatroomList.forEach { userHex, room ->
|
||||
room.chatrooms.map { key, chatroom ->
|
||||
val toBeRemoved = chatroom.pruneMessagesToTheLatestOnly()
|
||||
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
@@ -2456,7 +2263,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
if (toBeRemoved.size > 1) {
|
||||
println(
|
||||
"PRUNE: ${toBeRemoved.size} private messages from ${user.toBestDisplayName()} to ${it.authors.joinToString(", ") { it.toBestDisplayName() }} removed. ${it.roomMessages.size} kept",
|
||||
"PRUNE: ${toBeRemoved.size} private messages from $userHex to ${key.users.joinToString()} removed. ${chatroom.roomMessages.size} kept",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2582,6 +2389,8 @@ object LocalCache : ILocalCache {
|
||||
note.clearFlow()
|
||||
|
||||
notes.remove(note.idHex)
|
||||
|
||||
refreshDeletedNoteObservers(note)
|
||||
}
|
||||
|
||||
fun removeFromCache(nextToBeRemoved: List<Note>) {
|
||||
@@ -2665,11 +2474,14 @@ object LocalCache : ILocalCache {
|
||||
// Observers line up here.
|
||||
val live: LocalCacheFlow = LocalCacheFlow()
|
||||
|
||||
private fun refreshObservers(newNote: Note) {
|
||||
private fun refreshNewNoteObservers(newNote: Note) {
|
||||
val event = newNote.event as Event
|
||||
updateObservables(event)
|
||||
onNewEvents.forEach { it(newNote) }
|
||||
live.invalidateData(newNote)
|
||||
live.newNote(newNote)
|
||||
}
|
||||
|
||||
private fun refreshDeletedNoteObservers(newNote: Note) {
|
||||
live.removedNote(newNote)
|
||||
}
|
||||
|
||||
fun justVerify(event: Event): Boolean {
|
||||
@@ -2680,7 +2492,7 @@ object LocalCache : ILocalCache {
|
||||
event.checkSignature()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("Event Verification Failed", "Kind: ${event.kind} from ${dateFormatter(event.createdAt, "", "")} with message ${e.message}: ${event.toJson()}")
|
||||
Log.w("Event Verification Failed", "Kind: ${event.kind} from ${dateFormatter(event.createdAt, "", "")} with message ${e.message}")
|
||||
}
|
||||
false
|
||||
} else {
|
||||
@@ -2702,165 +2514,12 @@ object LocalCache : ILocalCache {
|
||||
val note = Note(event.id)
|
||||
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
|
||||
relay?.let { note.addRelay(it) }
|
||||
refreshObservers(note)
|
||||
refreshNewNoteObservers(note)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun indexDraftAsRealEvent(
|
||||
draftWrap: DraftEvent,
|
||||
draft: Event,
|
||||
) {
|
||||
val note = getOrCreateAddressableNote(draftWrap.address())
|
||||
val author = getOrCreateUser(draftWrap.pubKey)
|
||||
|
||||
when (draft) {
|
||||
is PrivateDmEvent -> {
|
||||
draft.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }?.let { recipient ->
|
||||
author.addMessage(recipient, note)
|
||||
recipient.addMessage(author, note)
|
||||
}
|
||||
}
|
||||
is ChatMessageEvent -> {
|
||||
val recipientsHex = draft.groupMembers()
|
||||
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
|
||||
|
||||
if (recipients.isNotEmpty()) {
|
||||
recipients.forEach {
|
||||
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
|
||||
|
||||
val authorGroup =
|
||||
if (groupMinusRecipient.isEmpty()) {
|
||||
// note to self
|
||||
ChatroomKey(persistentSetOf(it.pubkeyHex))
|
||||
} else {
|
||||
ChatroomKey(groupMinusRecipient.toImmutableSet())
|
||||
}
|
||||
|
||||
it.addMessage(authorGroup, note)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatMessageEncryptedFileHeaderEvent -> {
|
||||
val recipientsHex = draft.groupMembers()
|
||||
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
|
||||
|
||||
if (recipients.isNotEmpty()) {
|
||||
recipients.forEach {
|
||||
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
|
||||
|
||||
val authorGroup =
|
||||
if (groupMinusRecipient.isEmpty()) {
|
||||
// note to self
|
||||
ChatroomKey(persistentSetOf(it.pubkeyHex))
|
||||
} else {
|
||||
ChatroomKey(groupMinusRecipient.toImmutableSet())
|
||||
}
|
||||
|
||||
it.addMessage(authorGroup, note)
|
||||
}
|
||||
}
|
||||
}
|
||||
is EphemeralChatEvent -> {
|
||||
draft.roomId()?.let {
|
||||
getOrCreateEphemeralChannel(it).addNote(note, null)
|
||||
}
|
||||
}
|
||||
is ChannelMessageEvent -> {
|
||||
draft.channelId()?.let { channelId ->
|
||||
checkGetOrCreatePublicChatChannel(channelId)?.addNote(note, null)
|
||||
}
|
||||
}
|
||||
is LiveActivitiesChatMessageEvent -> {
|
||||
draft.activityAddress()?.let { channelId ->
|
||||
getOrCreateLiveChannel(channelId).addNote(note, null)
|
||||
}
|
||||
}
|
||||
is TextNoteEvent -> {
|
||||
val replyTo = computeReplyTo(draft)
|
||||
val author = getOrCreateUser(draftWrap.pubKey)
|
||||
note.loadEvent(draftWrap, author, replyTo)
|
||||
replyTo.forEach { it.addReply(note) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deindexDraftAsRealEvent(
|
||||
draftWrap: Note,
|
||||
draft: Event,
|
||||
) {
|
||||
val author = draftWrap.author ?: return
|
||||
|
||||
when (draft) {
|
||||
is PrivateDmEvent -> {
|
||||
draft.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }?.let { recipient ->
|
||||
author.removeMessage(recipient, draftWrap)
|
||||
recipient.removeMessage(author, draftWrap)
|
||||
}
|
||||
}
|
||||
is ChatMessageEvent -> {
|
||||
val recipientsHex = draft.recipientsPubKey().plus(author.pubkeyHex).toSet()
|
||||
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
|
||||
|
||||
if (recipients.isNotEmpty()) {
|
||||
recipients.forEach {
|
||||
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
|
||||
|
||||
val authorGroup =
|
||||
if (groupMinusRecipient.isEmpty()) {
|
||||
// note to self
|
||||
ChatroomKey(persistentSetOf(it.pubkeyHex))
|
||||
} else {
|
||||
ChatroomKey(groupMinusRecipient.toImmutableSet())
|
||||
}
|
||||
|
||||
it.removeMessage(authorGroup, draftWrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatMessageEncryptedFileHeaderEvent -> {
|
||||
val recipientsHex = draft.groupMembers()
|
||||
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
|
||||
|
||||
if (recipients.isNotEmpty()) {
|
||||
recipients.forEach {
|
||||
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
|
||||
|
||||
val authorGroup =
|
||||
if (groupMinusRecipient.isEmpty()) {
|
||||
// note to self
|
||||
ChatroomKey(persistentSetOf(it.pubkeyHex))
|
||||
} else {
|
||||
ChatroomKey(groupMinusRecipient.toImmutableSet())
|
||||
}
|
||||
|
||||
it.removeMessage(authorGroup, draftWrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChannelMessageEvent -> {
|
||||
draft.channelId()?.let { channelId ->
|
||||
getPublicChatChannelIfExists(channelId)?.removeNote(draftWrap)
|
||||
}
|
||||
}
|
||||
is EphemeralChatEvent -> {
|
||||
draft.roomId()?.let {
|
||||
getEphemeralChatChannelIfExists(it)?.removeNote(draftWrap)
|
||||
}
|
||||
}
|
||||
is LiveActivitiesChatMessageEvent -> {
|
||||
draft.activityAddress()?.let { channelId ->
|
||||
getLiveActivityChannelIfExists(channelId)?.removeNote(draftWrap)
|
||||
}
|
||||
}
|
||||
is TextNoteEvent -> {
|
||||
val replyTo = computeReplyTo(draft)
|
||||
replyTo.forEach { it.removeReply(draftWrap) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(nip19: Entity) {
|
||||
when (nip19) {
|
||||
is NSec -> getOrCreateUser(nip19.toPubKeyHex())
|
||||
@@ -3122,7 +2781,7 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
Log.w("LocalCache", "Cannot consume ${event.kind}", e)
|
||||
false
|
||||
}
|
||||
|
||||
@@ -3159,15 +2818,24 @@ object LocalCache : ILocalCache {
|
||||
|
||||
@Stable
|
||||
class LocalCacheFlow {
|
||||
private val _newEventBundles = MutableSharedFlow<Set<Note>>(0, 10, BufferOverflow.DROP_OLDEST)
|
||||
private val _newEventBundles = MutableSharedFlow<Set<Note>>(0, 100, BufferOverflow.DROP_OLDEST)
|
||||
val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view
|
||||
|
||||
private val _deletedEventBundles = MutableSharedFlow<Set<Note>>(0, 100, BufferOverflow.DROP_OLDEST)
|
||||
val deletedEventBundles = _deletedEventBundles.asSharedFlow() // read-only public view
|
||||
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledInsert<Note>(1000, Dispatchers.Default)
|
||||
|
||||
fun invalidateData(newNote: Note) {
|
||||
fun newNote(newNote: Note) {
|
||||
bundler.invalidateList(newNote) { bundledNewNotes ->
|
||||
_newEventBundles.emit(bundledNewNotes)
|
||||
}
|
||||
}
|
||||
|
||||
fun removedNote(newNote: Note) {
|
||||
bundler.invalidateList(newNote) { bundledNewNotes ->
|
||||
_deletedEventBundles.emit(bundledNewNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
@@ -35,7 +36,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
@@ -55,6 +55,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
@@ -71,14 +72,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.anyAsync
|
||||
import com.vitorpamplona.quartz.utils.containsAny
|
||||
import com.vitorpamplona.quartz.utils.launchAndWaitAll
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import java.math.BigDecimal
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Stable
|
||||
class AddressableNote(
|
||||
@@ -466,21 +464,13 @@ open class Note(
|
||||
val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent
|
||||
|
||||
if (zapResponseEvent != null) {
|
||||
val result =
|
||||
tryAndWait { continuation ->
|
||||
account.decryptZapPaymentResponseEvent(zapResponseEvent) { response ->
|
||||
if (
|
||||
response is PayInvoiceSuccessResponse &&
|
||||
account.isNIP47Author(zapResponseEvent.requestAuthor())
|
||||
) {
|
||||
continuation.resume(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
account.nip47SignerState.decryptResponse(zapResponseEvent)?.let { response ->
|
||||
val result = response is PayInvoiceSuccessResponse && account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor())
|
||||
|
||||
if (!hasSentOne && result == true) {
|
||||
hasSentOne = true
|
||||
onWasZappedByAuthor()
|
||||
if (!hasSentOne && result == true) {
|
||||
hasSentOne = true
|
||||
onWasZappedByAuthor()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,7 +503,7 @@ open class Note(
|
||||
// private events
|
||||
|
||||
// if has already decrypted
|
||||
val privateZap = zapRequest.cachedPrivateZap()
|
||||
val privateZap = account.privateZapsDecryptionCache.cachedPrivateZap(zapRequest)
|
||||
if (privateZap != null) {
|
||||
if (privateZap.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
|
||||
onWasZappedByAuthor()
|
||||
@@ -529,12 +519,7 @@ open class Note(
|
||||
|
||||
val result =
|
||||
anyAsync(parallelDecrypt) { pair ->
|
||||
val result =
|
||||
tryAndWait { continuation ->
|
||||
pair.first.decryptPrivateZap(account.signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first)
|
||||
|
||||
result?.pubKey == user.pubkeyHex && (option == null || option == pair.second?.zappedPollOption())
|
||||
}
|
||||
@@ -607,26 +592,21 @@ open class Note(
|
||||
startAmount: BigDecimal,
|
||||
paidInvoiceSet: LinkedHashSet<String>,
|
||||
zapPayments: List<Pair<Note, Note?>>,
|
||||
signer: NostrSigner,
|
||||
onReady: (BigDecimal) -> Unit,
|
||||
) {
|
||||
signerState: NwcSignerState,
|
||||
): BigDecimal {
|
||||
if (zapPayments.isEmpty()) {
|
||||
onReady(startAmount)
|
||||
return
|
||||
return startAmount
|
||||
}
|
||||
|
||||
var output: BigDecimal = startAmount
|
||||
|
||||
launchAndWaitAll(zapPayments) { next ->
|
||||
val result =
|
||||
tryAndWait { continuation ->
|
||||
processZapAmountFromResponse(
|
||||
next.first,
|
||||
next.second,
|
||||
continuation,
|
||||
signer,
|
||||
)
|
||||
}
|
||||
processZapAmountFromResponse(
|
||||
next.first,
|
||||
next.second,
|
||||
signerState,
|
||||
)
|
||||
|
||||
if (result != null && !paidInvoiceSet.contains(result.invoice)) {
|
||||
paidInvoiceSet.add(result.invoice)
|
||||
@@ -634,27 +614,25 @@ open class Note(
|
||||
}
|
||||
}
|
||||
|
||||
onReady(output)
|
||||
return output
|
||||
}
|
||||
|
||||
private fun processZapAmountFromResponse(
|
||||
private suspend fun processZapAmountFromResponse(
|
||||
paymentRequest: Note,
|
||||
paymentResponse: Note?,
|
||||
continuation: Continuation<InvoiceAmount?>,
|
||||
signer: NostrSigner,
|
||||
) {
|
||||
signerState: NwcSignerState,
|
||||
): InvoiceAmount? {
|
||||
val nwcRequest = paymentRequest.event as? LnZapPaymentRequestEvent
|
||||
val nwcResponse = paymentResponse?.event as? LnZapPaymentResponseEvent
|
||||
|
||||
if (nwcRequest != null && nwcResponse != null) {
|
||||
return if (nwcRequest != null && nwcResponse != null) {
|
||||
processZapAmountFromResponse(
|
||||
nwcRequest,
|
||||
nwcResponse,
|
||||
continuation,
|
||||
signer,
|
||||
signerState,
|
||||
)
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,18 +641,19 @@ open class Note(
|
||||
val amount: BigDecimal,
|
||||
)
|
||||
|
||||
private fun processZapAmountFromResponse(
|
||||
private suspend fun processZapAmountFromResponse(
|
||||
nwcRequest: LnZapPaymentRequestEvent,
|
||||
nwcResponse: LnZapPaymentResponseEvent,
|
||||
continuation: Continuation<InvoiceAmount?>,
|
||||
signer: NostrSigner,
|
||||
) {
|
||||
signerState: NwcSignerState,
|
||||
): InvoiceAmount? {
|
||||
// if we can decrypt the reply
|
||||
nwcResponse.response(signer) { noteEvent ->
|
||||
return signerState.decryptResponse(nwcResponse)?.let { noteEvent ->
|
||||
// if it is a sucess
|
||||
if (noteEvent is PayInvoiceSuccessResponse) {
|
||||
// if we can decrypt the invoice
|
||||
nwcRequest.lnInvoice(signer) { invoice ->
|
||||
val request = signerState.decryptRequest(nwcRequest)
|
||||
val invoice = (request as? PayInvoiceMethod)?.params?.invoice
|
||||
if (invoice != null) {
|
||||
// if we can parse the amount
|
||||
val amount =
|
||||
try {
|
||||
@@ -686,34 +665,32 @@ open class Note(
|
||||
|
||||
// avoid double counting
|
||||
if (amount != null) {
|
||||
continuation.resume(InvoiceAmount(invoice, amount))
|
||||
InvoiceAmount(invoice, amount)
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun zappedAmountWithNWCPayments(
|
||||
signer: NostrSigner,
|
||||
onReady: (BigDecimal) -> Unit,
|
||||
) {
|
||||
suspend fun zappedAmountWithNWCPayments(signerState: NwcSignerState): BigDecimal {
|
||||
if (zapPayments.isEmpty()) {
|
||||
onReady(zapsAmount)
|
||||
return zapsAmount
|
||||
}
|
||||
|
||||
val invoiceSet = LinkedHashSet<String>(zaps.size + zapPayments.size)
|
||||
zaps.forEach { (it.value?.event as? LnZapEvent)?.lnInvoice()?.let { invoiceSet.add(it) } }
|
||||
|
||||
zappedAmountCalculation(
|
||||
return zappedAmountCalculation(
|
||||
zapsAmount,
|
||||
invoiceSet,
|
||||
zapPayments.toList(),
|
||||
signer,
|
||||
onReady,
|
||||
signerState,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.quartz.lightning.Lud06
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -35,11 +34,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
@@ -47,7 +44,6 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.containsAny
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.math.BigDecimal
|
||||
|
||||
@@ -60,7 +56,6 @@ class User(
|
||||
var latestMetadata: MetadataEvent? = null
|
||||
var latestMetadataRelay: NormalizedRelayUrl? = null
|
||||
var latestContactList: ContactListEvent? = null
|
||||
var latestBookmarkList: BookmarkListEvent? = null
|
||||
|
||||
var reports = mapOf<User, Set<Note>>()
|
||||
private set
|
||||
@@ -71,9 +66,6 @@ class User(
|
||||
var relaysBeingUsed = mapOf<NormalizedRelayUrl, RelayInfo>()
|
||||
private set
|
||||
|
||||
var privateChatrooms = mapOf<ChatroomKey, Chatroom>()
|
||||
private set
|
||||
|
||||
fun pubkey() = Hex.decode(pubkeyHex)
|
||||
|
||||
fun pubkeyNpub() = pubkey().toNpub()
|
||||
@@ -124,13 +116,6 @@ class User(
|
||||
|
||||
fun profilePicture(): String? = info?.picture
|
||||
|
||||
fun updateBookmark(event: BookmarkListEvent) {
|
||||
if (event.id == latestBookmarkList?.id) return
|
||||
|
||||
latestBookmarkList = event
|
||||
flowSet?.bookmarks?.invalidateData()
|
||||
}
|
||||
|
||||
fun updateContactList(event: ContactListEvent) {
|
||||
if (event.id == latestContactList?.id) return
|
||||
|
||||
@@ -230,73 +215,6 @@ class User(
|
||||
}
|
||||
}.flatten()
|
||||
|
||||
@Synchronized
|
||||
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom =
|
||||
privateChatrooms[key]
|
||||
?: run {
|
||||
val privateChatroom = Chatroom()
|
||||
privateChatrooms = privateChatrooms + Pair(key, privateChatroom)
|
||||
privateChatroom
|
||||
}
|
||||
|
||||
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
||||
val key = ChatroomKey(persistentSetOf(user.pubkeyHex))
|
||||
return getOrCreatePrivateChatroom(key)
|
||||
}
|
||||
|
||||
private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key)
|
||||
|
||||
fun addMessage(
|
||||
room: ChatroomKey,
|
||||
msg: Note,
|
||||
) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(room)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.addMessageSync(msg)
|
||||
flowSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun addMessage(
|
||||
user: User,
|
||||
msg: Note,
|
||||
) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.addMessageSync(msg)
|
||||
flowSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun createChatroom(withKey: ChatroomKey) {
|
||||
getOrCreatePrivateChatroom(withKey)
|
||||
}
|
||||
|
||||
fun removeMessage(
|
||||
user: User,
|
||||
msg: Note,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.removeMessageSync(msg)
|
||||
flowSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMessage(
|
||||
room: ChatroomKey,
|
||||
msg: Note,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
val privateChatroom = getOrCreatePrivateChatroom(room)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.removeMessageSync(msg)
|
||||
flowSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun addRelayBeingUsed(
|
||||
relay: NormalizedRelayUrl,
|
||||
eventTime: Long,
|
||||
@@ -343,12 +261,6 @@ class User(
|
||||
|
||||
suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
|
||||
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
|
||||
val messagesToUser = privateChatrooms[key] ?: return false
|
||||
|
||||
return messagesToUser.authors.any { this == it }
|
||||
}
|
||||
|
||||
fun hasReport(
|
||||
loggedIn: User,
|
||||
type: ReportType,
|
||||
@@ -432,10 +344,8 @@ class UserFlowSet(
|
||||
val relays = UserBundledRefresherFlow(u)
|
||||
val followers = UserBundledRefresherFlow(u)
|
||||
val reports = UserBundledRefresherFlow(u)
|
||||
val messages = UserBundledRefresherFlow(u)
|
||||
val relayInfo = UserBundledRefresherFlow(u)
|
||||
val zaps = UserBundledRefresherFlow(u)
|
||||
val bookmarks = UserBundledRefresherFlow(u)
|
||||
val statuses = UserBundledRefresherFlow(u)
|
||||
|
||||
fun isInUse(): Boolean =
|
||||
@@ -444,10 +354,8 @@ class UserFlowSet(
|
||||
follows.hasObservers() ||
|
||||
followers.hasObservers() ||
|
||||
reports.hasObservers() ||
|
||||
messages.hasObservers() ||
|
||||
relayInfo.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
bookmarks.hasObservers() ||
|
||||
statuses.hasObservers()
|
||||
}
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.edits
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
|
||||
class PrivateStorageRelayListDecryptionCache(
|
||||
signer: NostrSigner,
|
||||
) : GenericRelayListCache<PrivateOutboxRelayListEvent>(signer)
|
||||
+11
-18
@@ -26,9 +26,9 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,6 +44,7 @@ import kotlinx.coroutines.launch
|
||||
class PrivateStorageRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: PrivateStorageRelayListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
@@ -55,9 +56,9 @@ class PrivateStorageRelayListState(
|
||||
|
||||
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
|
||||
|
||||
fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
suspend fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList
|
||||
return event?.relays()?.toSet() ?: emptySet()
|
||||
return event?.let { decryptionCache.relays(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
val flow =
|
||||
@@ -69,28 +70,22 @@ class PrivateStorageRelayListState(
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()),
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
onDone: (PrivateOutboxRelayListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun saveRelayList(relays: List<NormalizedRelayUrl>): PrivateOutboxRelayListEvent {
|
||||
val relayListForPrivateOutbox = getPrivateOutboxRelayList()
|
||||
|
||||
if (relayListForPrivateOutbox != null && !relayListForPrivateOutbox.cachedPrivateTags().isNullOrEmpty()) {
|
||||
return if (relayListForPrivateOutbox != null) {
|
||||
PrivateOutboxRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForPrivateOutbox,
|
||||
relays = relays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
PrivateOutboxRelayListEvent.createFromScratch(
|
||||
PrivateOutboxRelayListEvent.create(
|
||||
relays = relays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -100,17 +95,15 @@ class PrivateStorageRelayListState(
|
||||
Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start")
|
||||
getPrivateOutboxRelayListFlow().collect {
|
||||
getPrivateOutboxRelayListFlow().collect { noteState ->
|
||||
Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${signer.pubKey}")
|
||||
(it.note.event as? PrivateOutboxRelayListEvent)?.let {
|
||||
(noteState.note.event as? PrivateOutboxRelayListEvent)?.let {
|
||||
settings.updatePrivateHomeRelayList(it)
|
||||
}
|
||||
}
|
||||
|
||||
+17
-16
@@ -18,23 +18,24 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
package com.vitorpamplona.amethyst.model.emphChat
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.LoginResponse
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.roomSet
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
|
||||
class LoginResultProcessor(
|
||||
val onReady: (HexKey, String) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun consume(intent: Intent) {
|
||||
val foregroundResult = LoginResponse.parse(intent)
|
||||
class EphemeralChatListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<EphemeralChatListEvent>(signer)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<PubKeyResult>) {
|
||||
onReady(foregroundResult.result.pubkey, foregroundResult.result.packageName)
|
||||
}
|
||||
}
|
||||
fun cachedRoomSet(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).roomSet()
|
||||
|
||||
fun cachedRooms(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).rooms()
|
||||
|
||||
suspend fun roomSet(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagList(event).roomSet()
|
||||
|
||||
suspend fun rooms(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagList(event).rooms()
|
||||
}
|
||||
+17
-37
@@ -30,7 +30,6 @@ import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -43,11 +42,11 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class EphemeralChatListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: EphemeralChatListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
@@ -59,17 +58,10 @@ class EphemeralChatListState(
|
||||
|
||||
fun getEphemeralChatList(): EphemeralChatListEvent? = getEphemeralChatListNote().event as? EphemeralChatListEvent
|
||||
|
||||
suspend fun ephemeralChatListWithBackup(note: Note): Set<RoomId> =
|
||||
ephemeralChatList(
|
||||
note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList,
|
||||
)
|
||||
|
||||
suspend fun ephemeralChatList(event: EphemeralChatListEvent?): Set<RoomId> =
|
||||
tryAndWait { continuation ->
|
||||
event?.publicAndPrivateRoomIds(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
suspend fun ephemeralChatListWithBackup(note: Note): Set<RoomId> {
|
||||
val event = note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList
|
||||
return event?.let { decryptionCache.roomSet(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveEphemeralChatList: StateFlow<Set<RoomId>> =
|
||||
@@ -85,46 +77,36 @@ class EphemeralChatListState(
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun follow(
|
||||
channel: EphemeralChatChannel,
|
||||
onDone: (EphemeralChatListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(channel: EphemeralChatChannel): EphemeralChatListEvent {
|
||||
val ephemeralChatList = getEphemeralChatList()
|
||||
|
||||
if (ephemeralChatList == null) {
|
||||
EphemeralChatListEvent.createRoom(
|
||||
return if (ephemeralChatList == null) {
|
||||
EphemeralChatListEvent.create(
|
||||
room = channel.roomId,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
EphemeralChatListEvent.addRoom(
|
||||
EphemeralChatListEvent.add(
|
||||
earlierVersion = ephemeralChatList,
|
||||
room = channel.roomId,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
channel: EphemeralChatChannel,
|
||||
onDone: (EphemeralChatListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun unfollow(channel: EphemeralChatChannel): EphemeralChatListEvent? {
|
||||
val ephemeralChatList = getEphemeralChatList()
|
||||
|
||||
if (ephemeralChatList != null) {
|
||||
EphemeralChatListEvent.removeRoom(
|
||||
return if (ephemeralChatList != null) {
|
||||
EphemeralChatListEvent.remove(
|
||||
earlierVersion = ephemeralChatList,
|
||||
room = channel.roomId,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,17 +115,15 @@ class EphemeralChatListState(
|
||||
Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "EphemeralChatList Collector Start")
|
||||
getEphemeralChatListFlow().collect {
|
||||
getEphemeralChatListFlow().collect { noteState ->
|
||||
Log.d("AccountRegisterObservers", "EphemeralChatList List for ${signer.pubKey}")
|
||||
(it.note.event as? EphemeralChatListEvent)?.let {
|
||||
(noteState.note.event as? EphemeralChatListEvent)?.let {
|
||||
settings.updateEphemeralChatListTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -48,7 +48,7 @@ class UserMetadataState(
|
||||
|
||||
fun getUserMetadataEvent(): MetadataEvent? = getUserMetadataUser().latestMetadata
|
||||
|
||||
fun sendNewUserMetadata(
|
||||
suspend fun sendNewUserMetadata(
|
||||
name: String? = null,
|
||||
picture: String? = null,
|
||||
banner: String? = null,
|
||||
@@ -61,8 +61,7 @@ class UserMetadataState(
|
||||
twitter: String? = null,
|
||||
mastodon: String? = null,
|
||||
github: String? = null,
|
||||
onDone: (MetadataEvent) -> Unit,
|
||||
) {
|
||||
): MetadataEvent {
|
||||
val latest = getUserMetadataEvent()
|
||||
|
||||
val template =
|
||||
@@ -101,7 +100,7 @@ class UserMetadataState(
|
||||
)
|
||||
}
|
||||
|
||||
signer.sign(template, onDone)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
|
||||
+7
-15
@@ -113,39 +113,31 @@ class FollowListState(
|
||||
)
|
||||
}
|
||||
|
||||
fun follow(
|
||||
user: User,
|
||||
onDone: (ContactListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(user: User): ContactListEvent {
|
||||
val contactList = getFollowListEvent()
|
||||
|
||||
if (contactList != null) {
|
||||
ContactListEvent.followUser(contactList, user.pubkeyHex, signer, onReady = onDone)
|
||||
return if (contactList != null) {
|
||||
ContactListEvent.followUser(contactList, user.pubkeyHex, signer)
|
||||
} else {
|
||||
ContactListEvent.createFromScratch(
|
||||
followUsers = listOf(ContactTag(user.pubkeyHex, user.bestRelayHint(), null)),
|
||||
relayUse = emptyMap(),
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
user: User,
|
||||
onDone: (ContactListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun unfollow(user: User): ContactListEvent? {
|
||||
val contactList = getFollowListEvent()
|
||||
|
||||
if (contactList != null && contactList.tags.isNotEmpty()) {
|
||||
return if (contactList != null && contactList.tags.isNotEmpty()) {
|
||||
ContactListEvent.unfollowUser(
|
||||
contactList,
|
||||
user.pubkeyHex,
|
||||
signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
|
||||
+3
-10
@@ -71,25 +71,18 @@ class DmRelayListState(
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
dmRelays: List<NormalizedRelayUrl>,
|
||||
onDone: (ChatMessageRelayListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
suspend fun saveRelayList(dmRelays: List<NormalizedRelayUrl>): ChatMessageRelayListEvent {
|
||||
val relayListForDMs = getDMRelayList()
|
||||
if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) {
|
||||
return if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) {
|
||||
ChatMessageRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForDMs,
|
||||
relays = dmRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
ChatMessageRelayListEvent.createFromScratch(
|
||||
ChatMessageRelayListEvent.create(
|
||||
relays = dmRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -28,17 +28,15 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
|
||||
class RepostAction {
|
||||
companion object {
|
||||
fun repost(
|
||||
suspend fun repost(
|
||||
note: Note,
|
||||
signer: NostrSigner,
|
||||
onDone: (Event) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val noteEvent = note.event ?: return
|
||||
): Event? {
|
||||
val noteEvent = note.event ?: return null
|
||||
|
||||
if (note.hasBoostedInTheLast5Minutes(signer.pubKey)) {
|
||||
// has already bosted in the past 5mins
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
val noteHint = note.relayHintUrl()
|
||||
@@ -51,7 +49,7 @@ class RepostAction {
|
||||
GenericRepostEvent.build(noteEvent, noteHint, authorHint)
|
||||
}
|
||||
|
||||
signer.sign(template, onDone)
|
||||
return signer.sign(template)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-25
@@ -38,7 +38,7 @@ class ReactionAction {
|
||||
by: User,
|
||||
signer: NostrSigner,
|
||||
onPublic: (ReactionEvent) -> Unit,
|
||||
onPrivate: (NIP17Factory.Result) -> Unit,
|
||||
onPrivate: suspend (NIP17Factory.Result) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
@@ -55,14 +55,14 @@ class ReactionAction {
|
||||
val emojiUrl = EmojiUrlTag.decode(reaction)
|
||||
if (emojiUrl != null) {
|
||||
note.toEventHint<Event>()?.let {
|
||||
NIP17Factory().createReactionWithinGroup(
|
||||
emojiUrl = emojiUrl,
|
||||
originalNote = it,
|
||||
to = users,
|
||||
signer = signer,
|
||||
) {
|
||||
onPrivate(it)
|
||||
}
|
||||
onPrivate(
|
||||
NIP17Factory().createReactionWithinGroup(
|
||||
emojiUrl = emojiUrl,
|
||||
originalNote = it,
|
||||
to = users,
|
||||
signer = signer,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -70,14 +70,14 @@ class ReactionAction {
|
||||
}
|
||||
|
||||
note.toEventHint<Event>()?.let {
|
||||
NIP17Factory().createReactionWithinGroup(
|
||||
content = reaction,
|
||||
originalNote = it,
|
||||
to = users,
|
||||
signer = signer,
|
||||
) {
|
||||
onPrivate(it)
|
||||
}
|
||||
onPrivate(
|
||||
NIP17Factory().createReactionWithinGroup(
|
||||
content = reaction,
|
||||
originalNote = it,
|
||||
to = users,
|
||||
signer = signer,
|
||||
),
|
||||
)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
@@ -87,10 +87,7 @@ class ReactionAction {
|
||||
note.event?.let {
|
||||
val template = ReactionEvent.build(emojiUrl, EventHintBundle(it, note.relayHintUrl()))
|
||||
|
||||
signer.sign(
|
||||
template,
|
||||
onReady = onPublic,
|
||||
)
|
||||
onPublic(signer.sign(template))
|
||||
}
|
||||
|
||||
return
|
||||
@@ -98,10 +95,7 @@ class ReactionAction {
|
||||
}
|
||||
|
||||
note.toEventHint<Event>()?.let {
|
||||
signer.sign(
|
||||
ReactionEvent.build(reaction, it),
|
||||
onReady = onPublic,
|
||||
)
|
||||
onPublic(signer.sign(ReactionEvent.build(reaction, it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-16
@@ -18,23 +18,24 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
package com.vitorpamplona.amethyst.model.nip28PublicChats
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DecryptZapResponse
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.channelSet
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.channels
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
|
||||
class DecryptZapResultProcessor(
|
||||
val onReady: (LnZapPrivateEvent) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun consume(intent: Intent) {
|
||||
val foregroundResult = DecryptZapResponse.parse(intent)
|
||||
class PublicChatListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<ChannelListEvent>(signer)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<ZapEventDecryptionResult>) {
|
||||
onReady(foregroundResult.result.privateEvent)
|
||||
}
|
||||
}
|
||||
fun cachedChannelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channelSet()
|
||||
|
||||
fun cachedChannels(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channels()
|
||||
|
||||
suspend fun channelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channelSet()
|
||||
|
||||
suspend fun channels(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channels()
|
||||
}
|
||||
+22
-52
@@ -28,10 +28,9 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -45,11 +44,11 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class PublicChatListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: PublicChatListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
@@ -61,20 +60,13 @@ class PublicChatListState(
|
||||
|
||||
fun getChannelList(): ChannelListEvent? = getChannelListNote().event as? ChannelListEvent
|
||||
|
||||
suspend fun publicChatListWithBackup(note: Note): Set<EventIdHint> =
|
||||
publicChatList(
|
||||
note.event as? ChannelListEvent ?: settings.backupChannelList,
|
||||
)
|
||||
|
||||
suspend fun publicChatList(event: ChannelListEvent?): Set<EventIdHint> =
|
||||
tryAndWait { continuation ->
|
||||
event?.publicAndPrivateChannels(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
suspend fun publicChatListWithBackup(note: Note): Set<ChannelTag> {
|
||||
val event = note.event as? ChannelListEvent ?: settings.backupChannelList
|
||||
return event?.let { decryptionCache.channelSet(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<EventIdHint>> =
|
||||
val flow: StateFlow<Set<ChannelTag>> =
|
||||
getChannelListFlow()
|
||||
.transformLatest { noteState ->
|
||||
emit(publicChatListWithBackup(noteState.note))
|
||||
@@ -101,54 +93,34 @@ class PublicChatListState(
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun follow(
|
||||
channel: PublicChatChannel,
|
||||
onDone: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(channel: PublicChatChannel): ChannelListEvent {
|
||||
val publicChatList = getChannelList()
|
||||
|
||||
val fullHint = channel.toEventHint()
|
||||
if (fullHint != null) {
|
||||
if (publicChatList == null) {
|
||||
ChannelListEvent.createChannel(fullHint, true, signer, onReady = onDone)
|
||||
} else {
|
||||
ChannelListEvent.addChannel(publicChatList, fullHint, true, signer, onReady = onDone)
|
||||
}
|
||||
return if (publicChatList == null) {
|
||||
ChannelListEvent.create(ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer)
|
||||
} else {
|
||||
val partialHint = channel.toEventId()
|
||||
if (publicChatList == null) {
|
||||
ChannelListEvent.createChannel(partialHint, true, signer, onReady = onDone)
|
||||
} else {
|
||||
ChannelListEvent.addChannel(publicChatList, partialHint, true, signer, onReady = onDone)
|
||||
}
|
||||
ChannelListEvent.add(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun follow(
|
||||
channels: List<PublicChatChannel>,
|
||||
onDone: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(channels: List<PublicChatChannel>): ChannelListEvent {
|
||||
val publicChatList = getChannelList()
|
||||
|
||||
val partialHint = channels.map { it.toEventId() }
|
||||
if (publicChatList == null) {
|
||||
ChannelListEvent.createChannels(partialHint, true, signer, onReady = onDone)
|
||||
val channelTags = channels.map { ChannelTag(it.idHex, it.relayHintUrl()) }
|
||||
return if (publicChatList == null) {
|
||||
ChannelListEvent.create(channelTags, true, signer)
|
||||
} else {
|
||||
ChannelListEvent.addChannels(publicChatList, partialHint, true, signer, onReady = onDone)
|
||||
ChannelListEvent.add(publicChatList, channelTags, true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
channel: PublicChatChannel,
|
||||
onDone: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun unfollow(channel: PublicChatChannel): ChannelListEvent? {
|
||||
val publicChatList = getChannelList()
|
||||
|
||||
if (publicChatList != null) {
|
||||
ChannelListEvent.removeChannel(publicChatList, channel.idHex, signer, onReady = onDone)
|
||||
return if (publicChatList != null) {
|
||||
ChannelListEvent.remove(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,9 +129,7 @@ class PublicChatListState(
|
||||
Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-20
@@ -127,39 +127,29 @@ class EmojiPackState(
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun addEmojiPack(
|
||||
emojiPack: Note,
|
||||
onDone: (EmojiPackSelectionEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
suspend fun addEmojiPack(emojiPack: Note): EmojiPackSelectionEvent {
|
||||
val emojiPackEvent = emojiPack.event
|
||||
if (emojiPackEvent !is EmojiPackEvent) return
|
||||
if (emojiPackEvent !is EmojiPackEvent) throw IllegalArgumentException("Cannot add an emoji pack to this kind of event.")
|
||||
|
||||
val eventHint = emojiPack.toEventHint<EmojiPackEvent>() ?: return
|
||||
val eventHint = emojiPack.toEventHint<EmojiPackEvent>() ?: throw IllegalArgumentException("Cannot add an emoji pack to this kind of event.")
|
||||
|
||||
val usersEmojiList = getEmojiPackSelection()
|
||||
if (usersEmojiList == null) {
|
||||
return if (usersEmojiList == null) {
|
||||
val template = EmojiPackSelectionEvent.build(listOf(eventHint))
|
||||
signer.sign(template, onDone)
|
||||
signer.sign(template)
|
||||
} else {
|
||||
val template = EmojiPackSelectionEvent.add(usersEmojiList, eventHint)
|
||||
signer.sign(template, onDone)
|
||||
signer.sign(template)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEmojiPack(
|
||||
emojiPack: Note,
|
||||
onDone: (EmojiPackSelectionEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
val usersEmojiList = getEmojiPackSelection() ?: return
|
||||
suspend fun removeEmojiPack(emojiPack: Note): EmojiPackSelectionEvent? {
|
||||
val usersEmojiList = getEmojiPackSelection() ?: throw IllegalArgumentException("Cannot remove an emoji pack to this kind of event.")
|
||||
|
||||
val emojiPackEvent = emojiPack.event
|
||||
if (emojiPackEvent !is EmojiPackEvent) return
|
||||
if (emojiPackEvent !is EmojiPackEvent) return null
|
||||
|
||||
val template = EmojiPackSelectionEvent.remove(usersEmojiList, emojiPackEvent)
|
||||
signer.sign(template, onDone)
|
||||
return signer.sign(template)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-22
@@ -28,44 +28,35 @@ import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
|
||||
class UserStatusAction {
|
||||
companion object {
|
||||
fun create(
|
||||
suspend fun create(
|
||||
newStatus: String,
|
||||
signer: NostrSigner,
|
||||
onDone: (StatusEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
): StatusEvent = StatusEvent.create(newStatus, "general", expiration = null, signer)
|
||||
|
||||
StatusEvent.create(newStatus, "general", expiration = null, signer, onReady = onDone)
|
||||
}
|
||||
|
||||
fun update(
|
||||
suspend fun update(
|
||||
oldStatus: AddressableNote,
|
||||
newStatus: String,
|
||||
signer: NostrSigner,
|
||||
onDone: (StatusEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: return
|
||||
): StatusEvent {
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event")
|
||||
|
||||
StatusEvent.update(oldEvent, newStatus, signer, onReady = onDone)
|
||||
return StatusEvent.update(oldEvent, newStatus, signer)
|
||||
}
|
||||
|
||||
fun delete(
|
||||
suspend fun delete(
|
||||
oldStatus: AddressableNote,
|
||||
signer: NostrSigner,
|
||||
onDone: (Event) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: return
|
||||
): List<Event> {
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event")
|
||||
|
||||
StatusEvent.clear(oldEvent, signer) { event ->
|
||||
onDone(event)
|
||||
val event = StatusEvent.clear(oldEvent, signer)
|
||||
|
||||
val deletion =
|
||||
signer.sign(
|
||||
DeletionEvent.buildForVersionOnly(listOf(event)),
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
|
||||
return listOf(event, deletion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectRequestCache
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectResponseCache
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Manages NIP-47 (Nostr Wallet Connect) related signing operations and decryption cache for a given account.
|
||||
*
|
||||
* Key Responsibilities:
|
||||
*
|
||||
* - Dynamically creates a NIP-47 signer if the wallet setup changes in the account settings.
|
||||
* - Provides decryption caches to manage decrypted NIP-47 requests and responses efficiently.
|
||||
* - Handles creating of zap payment requests and waits for responses.
|
||||
*
|
||||
* @property signer the main Nostr signer used for general Nostr operations
|
||||
* @property cache the local cache for handling notes and events
|
||||
* @property scope the coroutine scope used for async operations
|
||||
* @property settings the account settings containing NIP-47 configuration
|
||||
*/
|
||||
class NwcSignerState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
/**
|
||||
* Derives a NIP-47 signer from the zap payment request in settings.
|
||||
* If there's no valid configuration, it defaults to the main signer.
|
||||
* Flows updates whenever settings change.
|
||||
*/
|
||||
val nip47Signer =
|
||||
settings.zapPaymentRequest
|
||||
.map {
|
||||
buildSigner(it) ?: signer
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
buildSigner(settings.zapPaymentRequest.value) ?: signer,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a dedicated request decryption cache for the NIP-47 signer.
|
||||
* Flows updates whenever the signer changes.
|
||||
*/
|
||||
val zapPaymentRequestDecryptionCache =
|
||||
nip47Signer
|
||||
.map {
|
||||
NostrWalletConnectRequestCache(it)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectRequestCache(nip47Signer.value))
|
||||
|
||||
/**
|
||||
* Creates a dedicated response decryption cache for the NIP-47 signer.
|
||||
* Flows updates whenever the signer changes.
|
||||
*/
|
||||
val zapPaymentResponseDecryptionCache =
|
||||
nip47Signer
|
||||
.map {
|
||||
NostrWalletConnectResponseCache(it)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectResponseCache(nip47Signer.value))
|
||||
|
||||
fun buildSigner(uri: Nip47WalletConnect.Nip47URINorm?) =
|
||||
uri?.secret?.hexToByteArray()?.let {
|
||||
NostrSignerInternal(KeyPair(it))
|
||||
}
|
||||
|
||||
fun hasWalletConnectSetup(): Boolean = settings.zapPaymentRequest.value != null
|
||||
|
||||
fun isNIP47Author(pubkeyHex: String?): Boolean = nip47Signer.value.pubKey == pubkeyHex
|
||||
|
||||
/**
|
||||
* Decrypts a NIP-47 payment request using the current signer.
|
||||
*
|
||||
* @param nwcRequest the NIP-47 payment request event to decrypt
|
||||
* @return the decrypted request or null if not set up or decryption fails
|
||||
*/
|
||||
suspend fun decryptRequest(nwcRequest: LnZapPaymentRequestEvent): Request? {
|
||||
if (!hasWalletConnectSetup()) return null
|
||||
return zapPaymentRequestDecryptionCache.value.decryptRequest(nwcRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a NIP-47 payment response using the current signer.
|
||||
*
|
||||
* @param nwsResponse the NIP-47 payment response event to decrypt
|
||||
* @return the decrypted response or null if not set up or decryption fails
|
||||
*/
|
||||
suspend fun decryptResponse(nwsResponse: LnZapPaymentResponseEvent): Response? {
|
||||
if (!hasWalletConnectSetup()) return null
|
||||
return zapPaymentResponseDecryptionCache.value.decryptResponse(nwsResponse)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a zap payment request to a connected Lightning wallet.
|
||||
* Subscribes to responses and waits up to 60s for a reply.
|
||||
*
|
||||
* @param bolt11 the BOLT-11 invoice to pay
|
||||
* @param zappedNote the note being zapped (if any)
|
||||
* @param onResponse callback to handle the response from the wallet
|
||||
* @return a pair containing the payment request event and target relay URL
|
||||
* @throws IllegalArgumentException if no NIP-47 wallet is set up
|
||||
*/
|
||||
suspend fun sendZapPaymentRequestFor(
|
||||
bolt11: String,
|
||||
zappedNote: Note?,
|
||||
onResponse: (Response?) -> Unit,
|
||||
): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> {
|
||||
val walletService = settings.zapPaymentRequest.value
|
||||
if (walletService == null) throw IllegalArgumentException("No NIP47 setup")
|
||||
|
||||
val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, signer)
|
||||
|
||||
val filter =
|
||||
NWCPaymentQueryState(
|
||||
fromServiceHex = walletService.pubKeyHex,
|
||||
toUserHex = event.pubKey,
|
||||
replyingToHex = event.id,
|
||||
relay = walletService.relayUri,
|
||||
)
|
||||
|
||||
Amethyst.instance.sources.nwc
|
||||
.subscribe(filter)
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
delay(60000) // waits 1 minute to complete payment.
|
||||
Amethyst.instance.sources.nwc
|
||||
.unsubscribe(filter)
|
||||
}
|
||||
|
||||
cache.consume(event, zappedNote, true, walletService.relayUri) {
|
||||
onResponse(decryptResponse(it))
|
||||
}
|
||||
|
||||
return Pair(event, walletService.relayUri)
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class BookmarkListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
class BookmarkList(
|
||||
val public: List<Note> = emptyList(),
|
||||
val private: List<Note> = emptyList(),
|
||||
)
|
||||
|
||||
fun getBookmarkListAddress() = BookmarkListEvent.createBookmarkAddress(signer.pubKey)
|
||||
|
||||
fun getBookmarkListNote() = cache.getOrCreateAddressableNote(getBookmarkListAddress())
|
||||
|
||||
fun getBookmarkListFlow(): StateFlow<NoteState> = getBookmarkListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBookmarkList(): BookmarkListEvent? = getBookmarkListNote().event as? BookmarkListEvent
|
||||
|
||||
suspend fun publicBookmarks(note: Note): List<BookmarkIdTag> {
|
||||
val noteEvent = note.event as? BookmarkListEvent
|
||||
return noteEvent?.publicBookmarks() ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun privateBookmarks(note: Note): List<BookmarkIdTag> {
|
||||
val noteEvent = note.event as? BookmarkListEvent
|
||||
return noteEvent?.privateBookmarks(signer) ?: emptyList()
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val publicBookmarks: StateFlow<List<BookmarkIdTag>> =
|
||||
getBookmarkListFlow()
|
||||
.map { noteState ->
|
||||
publicBookmarks(noteState.note)
|
||||
}.onStart {
|
||||
emit(publicBookmarks(getBookmarkListNote()))
|
||||
}.debounce(100)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val privateBookmarks: StateFlow<List<BookmarkIdTag>> =
|
||||
getBookmarkListFlow()
|
||||
.map { noteState ->
|
||||
privateBookmarks(noteState.note)
|
||||
}.onStart {
|
||||
emit(privateBookmarks(getBookmarkListNote()))
|
||||
}.debounce(100)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val publicBookmarkEventIdSet =
|
||||
publicBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is EventBookmark) it.eventId else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val publicBookmarkAddressIdSet =
|
||||
publicBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is AddressBookmark) it.address else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val privateBookmarkEventIdSet =
|
||||
privateBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is EventBookmark) it.eventId else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val privateBookmarkAddressIdSet =
|
||||
privateBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is AddressBookmark) it.address else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun bookmarkList(
|
||||
privateBookmarks: List<BookmarkIdTag>,
|
||||
publicBookmarks: List<BookmarkIdTag>,
|
||||
): BookmarkList =
|
||||
BookmarkList(
|
||||
public =
|
||||
publicBookmarks
|
||||
.mapNotNull {
|
||||
when (it) {
|
||||
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
|
||||
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
|
||||
}
|
||||
}.reversed(),
|
||||
private =
|
||||
privateBookmarks
|
||||
.mapNotNull {
|
||||
when (it) {
|
||||
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
|
||||
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
|
||||
}
|
||||
}.reversed(),
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val bookmarks: StateFlow<BookmarkList> =
|
||||
combineTransform(privateBookmarks, publicBookmarks) { private, public ->
|
||||
emit(bookmarkList(private, public))
|
||||
}.onStart {
|
||||
emit(bookmarkList(privateBookmarks.value, publicBookmarks.value))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
BookmarkList(),
|
||||
)
|
||||
|
||||
fun isInPrivateBookmarks(note: Note): Boolean {
|
||||
if (!signer.isWriteable()) return false
|
||||
|
||||
return if (note is AddressableNote) {
|
||||
privateBookmarkAddressIdSet.value.contains(note.address)
|
||||
} else {
|
||||
privateBookmarkEventIdSet.value.contains(note.idHex)
|
||||
}
|
||||
}
|
||||
|
||||
fun isInPublicBookmarks(note: Note): Boolean =
|
||||
if (note is AddressableNote) {
|
||||
publicBookmarkAddressIdSet.value.contains(note.address)
|
||||
} else {
|
||||
publicBookmarkEventIdSet.value.contains(note.idHex)
|
||||
}
|
||||
|
||||
suspend fun addBookmark(
|
||||
note: Note,
|
||||
isPrivate: Boolean,
|
||||
): BookmarkListEvent {
|
||||
val bookmarkList = getBookmarkList()
|
||||
|
||||
return if (bookmarkList == null) {
|
||||
if (note is AddressableNote) {
|
||||
BookmarkListEvent.create(
|
||||
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
BookmarkListEvent.create(
|
||||
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (note is AddressableNote) {
|
||||
BookmarkListEvent.add(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
BookmarkListEvent.add(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeBookmark(
|
||||
note: Note,
|
||||
isPrivate: Boolean,
|
||||
): BookmarkListEvent? {
|
||||
val bookmarkList = getBookmarkList()
|
||||
|
||||
return if (bookmarkList != null) {
|
||||
if (note is AddressableNote) {
|
||||
BookmarkListEvent.remove(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
BookmarkListEvent.remove(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -24,7 +24,9 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -38,8 +40,8 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class HiddenUsersState(
|
||||
val muteList: StateFlow<PeopleListEvent.UsersAndWords>,
|
||||
val blockList: StateFlow<PeopleListEvent.UsersAndWords>,
|
||||
val muteList: StateFlow<List<MuteTag>>,
|
||||
val blockList: StateFlow<List<MuteTag>>,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
@@ -61,14 +63,14 @@ class HiddenUsersState(
|
||||
}
|
||||
|
||||
suspend fun assembleLiveHiddenUsers(
|
||||
blockList: PeopleListEvent.UsersAndWords,
|
||||
muteList: PeopleListEvent.UsersAndWords,
|
||||
blockList: List<MuteTag>,
|
||||
muteList: List<MuteTag>,
|
||||
transientHiddenUsers: Set<String>,
|
||||
showSensitiveContent: Boolean?,
|
||||
): LiveHiddenUsers =
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = blockList.users + muteList.users,
|
||||
hiddenWords = blockList.words + muteList.words,
|
||||
hiddenUsers = blockList.mapNotNullTo(mutableSetOf()) { if (it is UserTag) it.pubKey else null } + muteList.mapNotNull { if (it is UserTag) it.pubKey else null },
|
||||
hiddenWords = blockList.mapNotNullTo(mutableSetOf()) { if (it is WordTag) it.word else null } + muteList.mapNotNull { if (it is WordTag) it.word else null },
|
||||
spammers = transientHiddenUsers,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
)
|
||||
|
||||
+29
-67
@@ -18,14 +18,16 @@
|
||||
* 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.model.nip51Lists
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -34,11 +36,11 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class BlockPeopleListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: PeopleListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun getBlockListAddress() = PeopleListEvent.createBlockAddress(signer.pubKey)
|
||||
@@ -49,17 +51,10 @@ class BlockPeopleListState(
|
||||
|
||||
fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
|
||||
|
||||
suspend fun blockListWithBackup(note: Note): PeopleListEvent.UsersAndWords =
|
||||
blockList(
|
||||
note.event as? PeopleListEvent,
|
||||
)
|
||||
|
||||
suspend fun blockList(event: PeopleListEvent?): PeopleListEvent.UsersAndWords =
|
||||
tryAndWait { continuation ->
|
||||
event?.publicAndPrivateUsersAndWords(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: PeopleListEvent.UsersAndWords()
|
||||
suspend fun blockListWithBackup(note: Note): List<MuteTag> {
|
||||
val event = note.event as? PeopleListEvent
|
||||
return event?.let { decryptionCache.usersAndWords(it) } ?: emptyList()
|
||||
}
|
||||
|
||||
val flow =
|
||||
getBlockListFlow()
|
||||
@@ -69,88 +64,55 @@ class BlockPeopleListState(
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
PeopleListEvent.UsersAndWords(),
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun hideUser(
|
||||
pubkeyHex: String,
|
||||
onDone: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
suspend fun hideUser(pubkeyHex: String): PeopleListEvent {
|
||||
val blockList = getBlockList()
|
||||
|
||||
if (blockList != null) {
|
||||
PeopleListEvent.addUser(
|
||||
return if (blockList != null) {
|
||||
PeopleListEvent.add(
|
||||
earlierVersion = blockList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
person = UserTag(pubkeyHex),
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
PeopleListEvent.createListWithUser(
|
||||
PeopleListEvent.create(
|
||||
name = PeopleListEvent.BLOCK_LIST_D_TAG,
|
||||
pubKeyHex = pubkeyHex,
|
||||
person = UserTag(pubkeyHex),
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
dTag = PeopleListEvent.BLOCK_LIST_D_TAG,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showUser(
|
||||
pubkeyHex: String,
|
||||
onDone: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
suspend fun showUser(pubkeyHex: String): PeopleListEvent? {
|
||||
val blockList = getBlockList()
|
||||
|
||||
if (blockList != null) {
|
||||
PeopleListEvent.removeUser(
|
||||
return if (blockList != null) {
|
||||
PeopleListEvent.remove(
|
||||
earlierVersion = blockList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
person = UserTag(pubkeyHex),
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideWord(
|
||||
word: String,
|
||||
onDone: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
val blockList = getBlockList()
|
||||
|
||||
if (blockList != null) {
|
||||
PeopleListEvent.addWord(
|
||||
earlierVersion = blockList,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
PeopleListEvent.createListWithWord(
|
||||
name = PeopleListEvent.BLOCK_LIST_D_TAG,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun showWord(
|
||||
word: String,
|
||||
onDone: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
suspend fun showWord(word: String): PeopleListEvent? {
|
||||
val blockList = getBlockList()
|
||||
|
||||
if (blockList != null) {
|
||||
PeopleListEvent.removeWord(
|
||||
return if (blockList != null) {
|
||||
PeopleListEvent.remove(
|
||||
earlierVersion = blockList,
|
||||
word = word,
|
||||
person = WordTag(word),
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUserIdSet
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsers
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsersAndWords
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWordSet
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWords
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
|
||||
class PeopleListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<PeopleListEvent>(signer)
|
||||
|
||||
fun cachedUsersAndWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsersAndWords()
|
||||
|
||||
fun cachedUsers(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsers()
|
||||
|
||||
fun cachedUserIdSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUserIdSet()
|
||||
|
||||
fun cachedWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWords()
|
||||
|
||||
fun cachedWordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWordSet()
|
||||
|
||||
suspend fun usersAndWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsersAndWords()
|
||||
|
||||
suspend fun users(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsers()
|
||||
|
||||
suspend fun userIdSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUserIdSet()
|
||||
|
||||
suspend fun words(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWords()
|
||||
|
||||
suspend fun wordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet()
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
|
||||
class BlockedRelayListDecryptionCache(
|
||||
signer: NostrSigner,
|
||||
) : GenericRelayListCache<BlockedRelayListEvent>(signer)
|
||||
+13
-16
@@ -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.model.nip51Lists
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
@@ -28,7 +28,8 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,10 +45,11 @@ import kotlinx.coroutines.launch
|
||||
class BlockedRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: BlockedRelayListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getBlockedRelayListAddress() = BlockedRelayListEvent.createAddress(signer.pubKey)
|
||||
fun getBlockedRelayListAddress() = BlockedRelayListEvent.Companion.createAddress(signer.pubKey)
|
||||
|
||||
fun getBlockedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlockedRelayListAddress())
|
||||
|
||||
@@ -55,9 +57,9 @@ class BlockedRelayListState(
|
||||
|
||||
fun getBlockedRelayList(): BlockedRelayListEvent? = getBlockedRelayListNote().event as? BlockedRelayListEvent
|
||||
|
||||
fun normalizeBlockedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
suspend fun normalizeBlockedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? BlockedRelayListEvent ?: settings.backupBlockedRelayList
|
||||
return event?.relays()?.toSet() ?: emptySet()
|
||||
return event?.let { decryptionCache.relays(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
val flow =
|
||||
@@ -67,29 +69,24 @@ class BlockedRelayListState(
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
blockedRelays: List<NormalizedRelayUrl>,
|
||||
onDone: (BlockedRelayListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun saveRelayList(blockedRelays: List<NormalizedRelayUrl>): BlockedRelayListEvent {
|
||||
if (!signer.isWriteable()) throw SignerExceptions.ReadOnlyException()
|
||||
val relayListForBlocked = getBlockedRelayList()
|
||||
|
||||
if (relayListForBlocked != null && relayListForBlocked.tags.isNotEmpty()) {
|
||||
BlockedRelayListEvent.updateRelayList(
|
||||
return if (relayListForBlocked != null && relayListForBlocked.tags.isNotEmpty()) {
|
||||
BlockedRelayListEvent.Companion.updateRelayList(
|
||||
earlierVersion = relayListForBlocked,
|
||||
relays = blockedRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
BlockedRelayListEvent.createFromScratch(
|
||||
BlockedRelayListEvent.Companion.create(
|
||||
relays = blockedRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -18,10 +18,10 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists
|
||||
|
||||
import android.content.Intent
|
||||
data class GeohashListCard(
|
||||
val relays: List<String>,
|
||||
)
|
||||
|
||||
interface NewResultProcessor {
|
||||
fun consume(intent: Intent)
|
||||
}
|
||||
val EmptyGeohashListCard = GeohashListCard(emptyList())
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.geohashSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
class GeohashListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<GeohashListEvent>(signer)
|
||||
|
||||
fun cachedGeohashes(event: GeohashListEvent) = cachedPrivateLists.mergeTagListPrecached(event).geohashSet()
|
||||
|
||||
suspend fun geohashes(event: GeohashListEvent) = cachedPrivateLists.mergeTagList(event).geohashSet()
|
||||
|
||||
fun fastStartValueForGeohashList(note: Note): GeohashListCard {
|
||||
val noteEvent = note.event as? GeohashListEvent
|
||||
return if (noteEvent != null) {
|
||||
GeohashListCard(cachedGeohashes(noteEvent).toList())
|
||||
} else {
|
||||
EmptyGeohashListCard
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun observeDecryptedGeohashList(note: Note): Flow<GeohashListCard> =
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { noteState ->
|
||||
val event = noteState.note.event as? GeohashListEvent
|
||||
GeohashListCard(event?.let { geohashes(it).toList() } ?: emptyList())
|
||||
}.onStart {
|
||||
val event = note.event as? GeohashListEvent
|
||||
if (event != null) {
|
||||
val list = geohashes(event)
|
||||
if (list.isNotEmpty()) {
|
||||
emit(GeohashListCard(list.toList()))
|
||||
} else {
|
||||
emit(EmptyGeohashListCard)
|
||||
}
|
||||
} else {
|
||||
emit(EmptyGeohashListCard)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
+23
-43
@@ -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.model.nip51Lists
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
@@ -27,8 +27,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -41,11 +40,11 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class GeohashListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: GeohashListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
@@ -57,17 +56,10 @@ class GeohashListState(
|
||||
|
||||
fun getGeohashList(): GeohashListEvent? = getGeohashListNote().event as? GeohashListEvent
|
||||
|
||||
suspend fun geohashListWithBackup(note: Note): Set<String> =
|
||||
geohashList(
|
||||
note.event as? GeohashListEvent ?: settings.backupGeohashList,
|
||||
)
|
||||
|
||||
suspend fun geohashList(event: GeohashListEvent?): Set<String> =
|
||||
tryAndWait { continuation ->
|
||||
event?.publicAndPrivateGeohash(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
suspend fun geohashListWithBackup(note: Note): Set<String> {
|
||||
val event = note.event as? GeohashListEvent ?: settings.backupGeohashList
|
||||
return event?.let { decryptionCache.geohashes(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<String>> =
|
||||
@@ -83,43 +75,33 @@ class GeohashListState(
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun follow(
|
||||
geohashs: List<String>,
|
||||
onDone: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(geohashes: List<String>): GeohashListEvent {
|
||||
val geohashList = getGeohashList()
|
||||
|
||||
if (geohashList == null) {
|
||||
GeohashListEvent.createGeohashs(geohashs, true, signer, onReady = onDone)
|
||||
return if (geohashList == null) {
|
||||
GeohashListEvent.create(geohashes, true, signer)
|
||||
} else {
|
||||
GeohashListEvent.addGeohashs(geohashList, geohashs, true, signer, onReady = onDone)
|
||||
GeohashListEvent.add(geohashList, geohashes, true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun follow(
|
||||
geohash: String,
|
||||
onDone: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(geohash: String): GeohashListEvent {
|
||||
val geohashList = getGeohashList()
|
||||
|
||||
if (geohashList == null) {
|
||||
GeohashListEvent.createGeohash(geohash, true, signer, onReady = onDone)
|
||||
return if (geohashList == null) {
|
||||
GeohashListEvent.create(geohash, true, signer)
|
||||
} else {
|
||||
GeohashListEvent.addGeohash(geohashList, geohash, true, signer, onReady = onDone)
|
||||
GeohashListEvent.add(geohashList, geohash, true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
geohash: String,
|
||||
onDone: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun unfollow(geohash: String): GeohashListEvent? {
|
||||
val geohashList = getGeohashList()
|
||||
|
||||
if (geohashList != null) {
|
||||
GeohashListEvent.removeGeohash(geohashList, geohash, signer, onReady = onDone)
|
||||
return if (geohashList != null) {
|
||||
GeohashListEvent.remove(geohashList, geohash, signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,17 +110,15 @@ class GeohashListState(
|
||||
Log.d("AccountRegisterObservers", "Loading saved Geohash list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Geohash List Collector Start")
|
||||
getGeohashListFlow().collect {
|
||||
getGeohashListFlow().collect { noteState ->
|
||||
Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}")
|
||||
(it.note.event as? GeohashListEvent)?.let {
|
||||
(noteState.note.event as? GeohashListEvent)?.let {
|
||||
settings.updateGeohashListTo(it)
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.hashtagSet
|
||||
|
||||
class HashtagListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<HashtagListEvent>(signer)
|
||||
|
||||
fun cachedHashtags(event: HashtagListEvent) = cachedPrivateLists.mergeTagListPrecached(event).hashtagSet()
|
||||
|
||||
suspend fun hashtags(event: HashtagListEvent) = cachedPrivateLists.mergeTagList(event).hashtagSet()
|
||||
}
|
||||
+23
-43
@@ -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.model.nip51Lists
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
@@ -27,8 +27,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -41,15 +40,15 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class HashtagListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: HashtagListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey)
|
||||
fun getHashtagListAddress() = HashtagListEvent.Companion.createAddress(signer.pubKey)
|
||||
|
||||
fun getHashtagListNote(): AddressableNote = cache.getOrCreateAddressableNote(getHashtagListAddress())
|
||||
|
||||
@@ -57,17 +56,10 @@ class HashtagListState(
|
||||
|
||||
fun getHashtagList(): HashtagListEvent? = getHashtagListNote().event as? HashtagListEvent
|
||||
|
||||
suspend fun hashtagListWithBackup(note: Note): Set<String> =
|
||||
hashtagList(
|
||||
note.event as? HashtagListEvent ?: settings.backupHashtagList,
|
||||
)
|
||||
|
||||
suspend fun hashtagList(event: HashtagListEvent?): Set<String> =
|
||||
tryAndWait { continuation ->
|
||||
event?.publicAndPrivateHashtag(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
suspend fun hashtagListWithBackup(note: Note): Set<String> {
|
||||
val event = note.event as? HashtagListEvent ?: settings.backupHashtagList
|
||||
return event?.let { decryptionCache.hashtags(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<String>> =
|
||||
@@ -79,47 +71,37 @@ class HashtagListState(
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun follow(
|
||||
hashtags: List<String>,
|
||||
onDone: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(hashtags: List<String>): HashtagListEvent {
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
if (hashtagList == null) {
|
||||
HashtagListEvent.createHashtags(hashtags, true, signer, onReady = onDone)
|
||||
return if (hashtagList == null) {
|
||||
HashtagListEvent.Companion.create(hashtags, true, signer)
|
||||
} else {
|
||||
HashtagListEvent.addHashtags(hashtagList, hashtags, true, signer, onReady = onDone)
|
||||
HashtagListEvent.Companion.add(hashtagList, hashtags, true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun follow(
|
||||
hashtag: String,
|
||||
onDone: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(hashtag: String): HashtagListEvent {
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
if (hashtagList == null) {
|
||||
HashtagListEvent.createHashtag(hashtag, true, signer, onReady = onDone)
|
||||
return if (hashtagList == null) {
|
||||
HashtagListEvent.Companion.create(hashtag, true, signer)
|
||||
} else {
|
||||
HashtagListEvent.addHashtag(hashtagList, hashtag, true, signer, onReady = onDone)
|
||||
HashtagListEvent.Companion.add(hashtagList, hashtag, true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
hashtag: String,
|
||||
onDone: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun unfollow(hashtag: String): HashtagListEvent? {
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
if (hashtagList != null) {
|
||||
HashtagListEvent.removeHashtag(hashtagList, hashtag, signer, onReady = onDone)
|
||||
return if (hashtagList != null) {
|
||||
HashtagListEvent.Companion.remove(hashtagList, hashtag, signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,9 +110,7 @@ class HashtagListState(
|
||||
Log.d("AccountRegisterObservers", "Loading saved Hashtag list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.muteList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUserIdSet
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsers
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsersAndWords
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWordSet
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWords
|
||||
|
||||
class MuteListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<MuteListEvent>(signer)
|
||||
|
||||
fun cachedUsersAndWords(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsersAndWords()
|
||||
|
||||
fun cachedUsers(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsers()
|
||||
|
||||
fun cachedUserIdSet(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUserIdSet()
|
||||
|
||||
fun cachedWords(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWords()
|
||||
|
||||
fun cachedWordSet(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWordSet()
|
||||
|
||||
suspend fun mutedUsersAndWords(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsersAndWords()
|
||||
|
||||
suspend fun mutedUsers(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsers()
|
||||
|
||||
suspend fun mutedUserIdSet(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedUserIdSet()
|
||||
|
||||
suspend fun mutedWords(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedWords()
|
||||
|
||||
suspend fun mutedWordSet(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet()
|
||||
}
|
||||
+45
-67
@@ -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.model.nip51Lists
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.muteList
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
@@ -26,9 +26,10 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -40,11 +41,11 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class MuteListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: MuteListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
@@ -56,17 +57,10 @@ class MuteListState(
|
||||
|
||||
fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
|
||||
|
||||
suspend fun muteListWithBackup(note: Note): PeopleListEvent.UsersAndWords =
|
||||
muteList(
|
||||
note.event as? MuteListEvent ?: settings.backupMuteList,
|
||||
)
|
||||
|
||||
suspend fun muteList(event: MuteListEvent?): PeopleListEvent.UsersAndWords =
|
||||
tryAndWait { continuation ->
|
||||
event?.publicAndPrivateUsersAndWords(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: PeopleListEvent.UsersAndWords()
|
||||
suspend fun muteListWithBackup(note: Note): List<MuteTag> {
|
||||
val event = note.event as? MuteListEvent ?: settings.backupMuteList
|
||||
return event?.let { decryptionCache.mutedUsersAndWords(it) } ?: emptyList()
|
||||
}
|
||||
|
||||
val flow =
|
||||
getMuteListFlow()
|
||||
@@ -76,86 +70,72 @@ class MuteListState(
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
PeopleListEvent.UsersAndWords(),
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun hideUser(
|
||||
pubkeyHex: String,
|
||||
onDone: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
suspend fun hideUser(pubkeyHex: String): MuteListEvent {
|
||||
val muteList = getMuteList()
|
||||
|
||||
if (muteList != null) {
|
||||
MuteListEvent.addUser(
|
||||
return if (muteList != null) {
|
||||
MuteListEvent.add(
|
||||
earlierVersion = muteList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
mute = UserTag(pubkeyHex),
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
MuteListEvent.createListWithUser(
|
||||
pubKeyHex = pubkeyHex,
|
||||
MuteListEvent.create(
|
||||
mute = UserTag(pubkeyHex),
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showUser(
|
||||
pubkeyHex: String,
|
||||
onDone: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
suspend fun showUser(pubkeyHex: String): MuteListEvent? {
|
||||
val muteList = getMuteList()
|
||||
|
||||
if (muteList != null) {
|
||||
MuteListEvent.removeUser(
|
||||
return if (muteList != null) {
|
||||
MuteListEvent.remove(
|
||||
earlierVersion = muteList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
mute = UserTag(pubkeyHex),
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideWord(
|
||||
word: String,
|
||||
onDone: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
val muteList = getMuteList()
|
||||
|
||||
if (muteList != null) {
|
||||
MuteListEvent.addWord(
|
||||
earlierVersion = muteList,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
MuteListEvent.createListWithWord(
|
||||
word = word,
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun hideWord(word: String): MuteListEvent {
|
||||
val muteList = getMuteList()
|
||||
|
||||
return if (muteList != null) {
|
||||
MuteListEvent.add(
|
||||
earlierVersion = muteList,
|
||||
mute = WordTag(word),
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
MuteListEvent.create(
|
||||
mute = WordTag(word),
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showWord(
|
||||
word: String,
|
||||
onDone: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
suspend fun showWord(word: String): MuteListEvent? {
|
||||
val muteList = getMuteList()
|
||||
|
||||
if (muteList != null) {
|
||||
MuteListEvent.removeWord(
|
||||
return if (muteList != null) {
|
||||
MuteListEvent.remove(
|
||||
earlierVersion = muteList,
|
||||
word = word,
|
||||
mute = WordTag(word),
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,9 +144,7 @@ class MuteListState(
|
||||
Log.d("AccountRegisterObservers", "Loading saved mute list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.relayLists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relaySet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
open class GenericRelayListCache<T : PrivateTagArrayEvent>(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<T>(signer)
|
||||
|
||||
fun cachedRelays(event: T) = cachedPrivateLists.mergeTagListPrecached(event).relaySet()
|
||||
|
||||
suspend fun relays(event: T) = cachedPrivateLists.mergeTagList(event).relaySet()
|
||||
|
||||
fun fastStartValueForRelayList(note: Note): RelayListCard {
|
||||
val noteEvent = note.event as? T
|
||||
return if (noteEvent != null) {
|
||||
RelayListCard(cachedRelays(noteEvent).toList())
|
||||
} else {
|
||||
EmptyRelayListCard
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun observeDecryptedRelayList(note: Note): Flow<RelayListCard> =
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { noteState ->
|
||||
val event = noteState.note.event as? T
|
||||
RelayListCard(event?.let { relays(it).toList() } ?: emptyList())
|
||||
}.onStart {
|
||||
val event = note.event as? T
|
||||
if (event != null) {
|
||||
val list = relays(event)
|
||||
if (list.isNotEmpty()) {
|
||||
emit(RelayListCard(list.toList()))
|
||||
} else {
|
||||
emit(EmptyRelayListCard)
|
||||
}
|
||||
} else {
|
||||
emit(EmptyRelayListCard)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.relayLists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@Immutable
|
||||
data class RelayListCard(
|
||||
val relays: List<NormalizedRelayUrl>,
|
||||
)
|
||||
|
||||
val EmptyRelayListCard = RelayListCard(emptyList())
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
|
||||
class SearchRelayListDecryptionCache(
|
||||
signer: NostrSigner,
|
||||
) : GenericRelayListCache<SearchRelayListEvent>(signer)
|
||||
+10
-15
@@ -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.model.nip50Search
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
@@ -45,10 +45,11 @@ import kotlinx.coroutines.launch
|
||||
class SearchRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: SearchRelayListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getSearchRelayListAddress() = SearchRelayListEvent.createAddress(signer.pubKey)
|
||||
fun getSearchRelayListAddress() = SearchRelayListEvent.Companion.createAddress(signer.pubKey)
|
||||
|
||||
fun getSearchRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getSearchRelayListAddress())
|
||||
|
||||
@@ -56,9 +57,9 @@ class SearchRelayListState(
|
||||
|
||||
fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent
|
||||
|
||||
fun normalizeSearchRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
suspend fun normalizeSearchRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? SearchRelayListEvent ?: settings.backupSearchRelayList
|
||||
return event?.relays()?.toSet() ?: DefaultSearchRelayList
|
||||
return event?.let { decryptionCache.relays(it) } ?: DefaultSearchRelayList
|
||||
}
|
||||
|
||||
val flow =
|
||||
@@ -68,29 +69,23 @@ class SearchRelayListState(
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
searchRelays: List<NormalizedRelayUrl>,
|
||||
onDone: (SearchRelayListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun saveRelayList(searchRelays: List<NormalizedRelayUrl>): SearchRelayListEvent {
|
||||
val relayListForSearch = getSearchRelayList()
|
||||
|
||||
if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) {
|
||||
SearchRelayListEvent.updateRelayList(
|
||||
return if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) {
|
||||
SearchRelayListEvent.Companion.updateRelayList(
|
||||
earlierVersion = relayListForSearch,
|
||||
relays = searchRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
SearchRelayListEvent.createFromScratch(
|
||||
SearchRelayListEvent.Companion.create(
|
||||
relays = searchRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
|
||||
class TrustedRelayListDecryptionCache(
|
||||
signer: NostrSigner,
|
||||
) : GenericRelayListCache<TrustedRelayListEvent>(signer)
|
||||
+13
-18
@@ -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.model.nip51Lists
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
@@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,20 +44,21 @@ import kotlinx.coroutines.launch
|
||||
class TrustedRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: TrustedRelayListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getTrustedRelayListAddress() = TrustedRelayListEvent.createAddress(signer.pubKey)
|
||||
fun getTrustedRelayListAddress() = TrustedRelayListEvent.Companion.createAddress(signer.pubKey)
|
||||
|
||||
fun getTrustedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getTrustedRelayListAddress())
|
||||
fun getTrustedRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getTrustedRelayListAddress())
|
||||
|
||||
fun getTrustedRelayListFlow(): StateFlow<NoteState> = getTrustedRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getTrustedRelayList(): TrustedRelayListEvent? = getTrustedRelayListNote().event as? TrustedRelayListEvent
|
||||
|
||||
fun normalizeTrustedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
suspend fun normalizeTrustedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? TrustedRelayListEvent ?: settings.backupTrustedRelayList
|
||||
return event?.relays()?.toSet() ?: emptySet()
|
||||
return event?.let { decryptionCache.relays(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
val flow =
|
||||
@@ -67,29 +68,23 @@ class TrustedRelayListState(
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
trustedRelays: List<NormalizedRelayUrl>,
|
||||
onDone: (TrustedRelayListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun saveRelayList(trustedRelays: List<NormalizedRelayUrl>): TrustedRelayListEvent {
|
||||
val relayListForTrusted = getTrustedRelayList()
|
||||
|
||||
if (relayListForTrusted != null && relayListForTrusted.tags.isNotEmpty()) {
|
||||
TrustedRelayListEvent.updateRelayList(
|
||||
return if (relayListForTrusted != null && relayListForTrusted.tags.isNotEmpty()) {
|
||||
TrustedRelayListEvent.Companion.updateRelayList(
|
||||
earlierVersion = relayListForTrusted,
|
||||
relays = trustedRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
TrustedRelayListEvent.createFromScratch(
|
||||
TrustedRelayListEvent.Companion.create(
|
||||
relays = trustedRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -98,7 +93,7 @@ class TrustedRelayListState(
|
||||
settings.backupTrustedRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved Trusted relay list ${it.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
+8
-14
@@ -28,23 +28,20 @@ import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
|
||||
class ReportAction {
|
||||
companion object {
|
||||
fun report(
|
||||
suspend fun report(
|
||||
user: User,
|
||||
type: ReportType,
|
||||
by: User,
|
||||
signer: NostrSigner,
|
||||
onDone: (ReportEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
): ReportEvent? {
|
||||
if (user.hasReport(by, type)) {
|
||||
// has already reported this note
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
val template = ReportEvent.build(user.pubkeyHex, type)
|
||||
|
||||
signer.sign(template, onDone)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
suspend fun report(
|
||||
@@ -53,17 +50,14 @@ class ReportAction {
|
||||
content: String = "",
|
||||
by: User,
|
||||
signer: NostrSigner,
|
||||
onDone: (ReportEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
): ReportEvent? {
|
||||
if (note.hasReport(by, type)) {
|
||||
// has already reported this note
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
note.event?.let {
|
||||
signer.sign(ReportEvent.build(it, type), onDone)
|
||||
return note.event?.let {
|
||||
signer.sign(ReportEvent.build(it, type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-9
@@ -105,26 +105,19 @@ class Nip65RelayListState(
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
relays: List<AdvertisedRelayInfo>,
|
||||
onDone: (AdvertisedRelayListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
suspend fun saveRelayList(relays: List<AdvertisedRelayInfo>): AdvertisedRelayListEvent {
|
||||
val nip65RelayList = getNIP65RelayList()
|
||||
|
||||
if (nip65RelayList != null) {
|
||||
return if (nip65RelayList != null) {
|
||||
AdvertisedRelayListEvent.replaceRelayListWith(
|
||||
earlierVersion = nip65RelayList,
|
||||
newRelays = relays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
AdvertisedRelayListEvent.createFromScratch(
|
||||
relays = relays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip72Communities
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.communities
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.communityIdSet
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.communityIds
|
||||
|
||||
class CommunityListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<CommunityListEvent>(signer)
|
||||
|
||||
fun cachedCommunityIds(event: CommunityListEvent) = cachedPrivateLists.mergeTagListPrecached(event).communityIds()
|
||||
|
||||
fun cachedCommunityIdSet(event: CommunityListEvent) = cachedPrivateLists.mergeTagListPrecached(event).communityIdSet()
|
||||
|
||||
fun cachedCommunities(event: CommunityListEvent) = cachedPrivateLists.mergeTagListPrecached(event).communities()
|
||||
|
||||
suspend fun communityIds(event: CommunityListEvent) = cachedPrivateLists.mergeTagList(event).communityIds()
|
||||
|
||||
suspend fun communityIdSet(event: CommunityListEvent) = cachedPrivateLists.mergeTagList(event).communityIdSet()
|
||||
|
||||
suspend fun communities(event: CommunityListEvent) = cachedPrivateLists.mergeTagList(event).communities()
|
||||
}
|
||||
+38
-54
@@ -26,11 +26,10 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,11 +43,11 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class CommunityListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: CommunityListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
@@ -60,20 +59,13 @@ class CommunityListState(
|
||||
|
||||
fun getCommunityList(): CommunityListEvent? = getCommunityListNote().event as? CommunityListEvent
|
||||
|
||||
suspend fun communityListWithBackup(note: Note): Set<AddressHint> =
|
||||
communityList(
|
||||
note.event as? CommunityListEvent ?: settings.backupCommunityList,
|
||||
)
|
||||
|
||||
suspend fun communityList(event: CommunityListEvent?): Set<AddressHint> =
|
||||
tryAndWait { continuation ->
|
||||
event?.publicAndPrivateCommunities(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
suspend fun communityListWithBackup(note: Note): Set<CommunityTag> {
|
||||
val event = note.event as? CommunityListEvent ?: settings.backupCommunityList
|
||||
return event?.let { decryptionCache.communities(it).toSet() } ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<AddressHint>> =
|
||||
val flow: StateFlow<Set<CommunityTag>> =
|
||||
getCommunityListFlow()
|
||||
.transformLatest { noteState ->
|
||||
emit(communityListWithBackup(noteState.note))
|
||||
@@ -90,9 +82,9 @@ class CommunityListState(
|
||||
val flowSet: StateFlow<Set<String>> =
|
||||
flow
|
||||
.map { hint ->
|
||||
hint.mapTo(mutableSetOf()) { it.addressId }
|
||||
hint.mapTo(mutableSetOf()) { it.address.toValue() }
|
||||
}.onStart {
|
||||
emit(flow.value.mapTo(mutableSetOf()) { it.addressId })
|
||||
emit(flow.value.mapTo(mutableSetOf()) { it.address.toValue() })
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
@@ -100,54 +92,48 @@ class CommunityListState(
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun follow(
|
||||
communities: List<AddressableNote>,
|
||||
onDone: (CommunityListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(communities: List<AddressableNote>): CommunityListEvent {
|
||||
val communityList = getCommunityList()
|
||||
|
||||
val partialHint = communities.mapNotNull { it.toEventHint<CommunityDefinitionEvent>() }
|
||||
if (communityList == null) {
|
||||
CommunityListEvent.createCommunities(partialHint, true, signer, onReady = onDone)
|
||||
val communityTags =
|
||||
communities.mapNotNull { community ->
|
||||
if (community.address.kind == CommunityDefinitionEvent.KIND) {
|
||||
CommunityTag(community.address, community.relayHintUrl())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return if (communityList == null) {
|
||||
CommunityListEvent.create(communityTags, true, signer)
|
||||
} else {
|
||||
CommunityListEvent.addCommunities(communityList, partialHint, true, signer, onReady = onDone)
|
||||
CommunityListEvent.add(communityList, communityTags, true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun follow(
|
||||
community: AddressableNote,
|
||||
onDone: (CommunityListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun follow(community: AddressableNote): CommunityListEvent? {
|
||||
val communityList = getCommunityList()
|
||||
if (community.address.kind != CommunityDefinitionEvent.KIND) return communityList
|
||||
|
||||
val fullHint = community.toEventHint<CommunityDefinitionEvent>()
|
||||
if (fullHint != null) {
|
||||
if (communityList == null) {
|
||||
CommunityListEvent.createCommunity(fullHint, true, signer, onReady = onDone)
|
||||
} else {
|
||||
CommunityListEvent.addCommunity(communityList, fullHint, true, signer, onReady = onDone)
|
||||
}
|
||||
return if (communityList == null) {
|
||||
CommunityListEvent.create(
|
||||
CommunityTag(community.address, community.relayHintUrl()),
|
||||
true,
|
||||
signer,
|
||||
)
|
||||
} else {
|
||||
val partialHint = community.toATag()
|
||||
if (communityList == null) {
|
||||
CommunityListEvent.createCommunity(partialHint, true, signer, onReady = onDone)
|
||||
} else {
|
||||
CommunityListEvent.addCommunity(communityList, partialHint, true, signer, onReady = onDone)
|
||||
}
|
||||
CommunityListEvent.add(communityList, CommunityTag(community.address, community.relayHintUrl()), true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
community: AddressableNote,
|
||||
onDone: (CommunityListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun unfollow(community: AddressableNote): CommunityListEvent? {
|
||||
val communityList = getCommunityList()
|
||||
if (community.address.kind != CommunityDefinitionEvent.KIND) return communityList
|
||||
|
||||
if (communityList != null) {
|
||||
CommunityListEvent.removeCommunity(communityList, community.address.toValue(), signer, onReady = onDone)
|
||||
return if (communityList != null) {
|
||||
CommunityListEvent.remove(communityList, CommunityTag(community.address, community.relayHintUrl()), signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,9 +142,7 @@ class CommunityListState(
|
||||
Log.d("AccountRegisterObservers", "Loading saved Community list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-31
@@ -53,17 +53,14 @@ class AppSpecificState(
|
||||
|
||||
fun getAppSpecificDataFlow(): StateFlow<NoteState> = getAppSpecificDataNote().flow().metadata.stateFlow
|
||||
|
||||
fun saveNewAppSpecificData(onDone: (AppSpecificDataEvent) -> Unit) {
|
||||
suspend fun saveNewAppSpecificData(): AppSpecificDataEvent {
|
||||
val toInternal = settings.syncedSettings.toInternal()
|
||||
signer.nip44Encrypt(JsonMapper.mapper.writeValueAsString(toInternal), signer.pubKey) { encrypted ->
|
||||
AppSpecificDataEvent.create(
|
||||
dTag = APP_SPECIFIC_DATA_D_TAG,
|
||||
description = encrypted,
|
||||
otherTags = emptyArray(),
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
return AppSpecificDataEvent.create(
|
||||
dTag = APP_SPECIFIC_DATA_D_TAG,
|
||||
description = signer.nip44Encrypt(JsonMapper.mapper.writeValueAsString(toInternal), signer.pubKey),
|
||||
otherTags = emptyArray(),
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -72,16 +69,13 @@ class AppSpecificState(
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
signer.decrypt(event.content, event.pubKey) { decrypted ->
|
||||
try {
|
||||
val syncedSettings = JsonMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
settings.syncedSettings.updateFrom(syncedSettings)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e)
|
||||
e.printStackTrace()
|
||||
AccountSyncedSettingsInternal()
|
||||
}
|
||||
try {
|
||||
val decrypted = signer.decrypt(event.content, event.pubKey)
|
||||
val syncedSettings = JsonMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
settings.syncedSettings.updateFrom(syncedSettings)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,18 +85,13 @@ class AppSpecificState(
|
||||
getAppSpecificDataFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}")
|
||||
(it.note.event as? AppSpecificDataEvent)?.let {
|
||||
signer.decrypt(it.content, it.pubKey) { decrypted ->
|
||||
val syncedSettings =
|
||||
try {
|
||||
JsonMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e)
|
||||
e.printStackTrace()
|
||||
AccountSyncedSettingsInternal()
|
||||
}
|
||||
|
||||
val decrypted = signer.decrypt(it.content, it.pubKey)
|
||||
try {
|
||||
val syncedSettings = JsonMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
settings.updateAppSpecificData(it, syncedSettings)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-6
@@ -66,11 +66,7 @@ class FileStorageServerListState(
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun saveFileServersList(
|
||||
servers: List<String>,
|
||||
onDone: (FileServersEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
suspend fun saveFileServersList(servers: List<String>): FileServersEvent {
|
||||
val serverList = getFileServersList()
|
||||
|
||||
val template =
|
||||
@@ -80,6 +76,6 @@ class FileStorageServerListState(
|
||||
FileServersEvent.build(servers)
|
||||
}
|
||||
|
||||
signer.sign(template, onDone)
|
||||
return signer.sign(template)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-29
@@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -38,7 +37,6 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class BlossomServerListState(
|
||||
val signer: NostrSigner,
|
||||
@@ -70,26 +68,19 @@ class BlossomServerListState(
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun saveBlossomServersList(
|
||||
servers: List<String>,
|
||||
onDone: (BlossomServersEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
suspend fun saveBlossomServersList(servers: List<String>): BlossomServersEvent {
|
||||
val serverList = getBlossomServersList()
|
||||
|
||||
if (serverList != null && serverList.tags.isNotEmpty()) {
|
||||
return if (serverList != null && serverList.tags.isNotEmpty()) {
|
||||
BlossomServersEvent.updateRelayList(
|
||||
earlierVersion = serverList,
|
||||
relays = servers,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
BlossomServersEvent.createFromScratch(
|
||||
relays = servers,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -98,26 +89,10 @@ class BlossomServerListState(
|
||||
hash: HexKey,
|
||||
size: Long,
|
||||
alt: String,
|
||||
): BlossomAuthorizationEvent? {
|
||||
if (!signer.isWriteable()) return null
|
||||
|
||||
return tryAndWait { continuation ->
|
||||
BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
): BlossomAuthorizationEvent = BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, signer)
|
||||
|
||||
suspend fun createBlossomDeleteAuth(
|
||||
hash: HexKey,
|
||||
alt: String,
|
||||
): BlossomAuthorizationEvent? {
|
||||
if (!signer.isWriteable()) return null
|
||||
|
||||
return tryAndWait { continuation ->
|
||||
BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
): BlossomAuthorizationEvent? = BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer)
|
||||
}
|
||||
|
||||
+18
-8
@@ -18,22 +18,27 @@
|
||||
* 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.model
|
||||
package com.vitorpamplona.amethyst.model.privateChats
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Stable
|
||||
class Chatroom {
|
||||
var authors: Set<User> = setOf()
|
||||
var activeSenders: Set<User> = setOf()
|
||||
var roomMessages: Set<Note> = setOf()
|
||||
var subject: String? = null
|
||||
var subject = MutableStateFlow<String?>(null)
|
||||
var subjectCreatedAt: Long? = null
|
||||
var ownerSentMessage: Boolean = false
|
||||
var lastMessage: Note? = null
|
||||
|
||||
@Synchronized
|
||||
fun addMessageSync(msg: Note) {
|
||||
@@ -43,15 +48,20 @@ class Chatroom {
|
||||
roomMessages = roomMessages + msg
|
||||
|
||||
msg.author?.let { author ->
|
||||
if (author !in authors) {
|
||||
authors += author
|
||||
if (author !in activeSenders) {
|
||||
activeSenders += author
|
||||
}
|
||||
}
|
||||
|
||||
val createdAt = msg.createdAt() ?: 0
|
||||
if (createdAt > (lastMessage?.createdAt() ?: 0)) {
|
||||
lastMessage = msg
|
||||
}
|
||||
|
||||
val newSubject = msg.event?.subject()
|
||||
|
||||
if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) {
|
||||
subject = newSubject
|
||||
subject.tryEmit(newSubject)
|
||||
subjectCreatedAt = msg.createdAt()
|
||||
}
|
||||
}
|
||||
@@ -69,13 +79,13 @@ class Chatroom {
|
||||
.sortedBy { it.createdAt() }
|
||||
.lastOrNull()
|
||||
?.let {
|
||||
subject = it.event?.subject()
|
||||
subject.tryEmit(it.event?.subject())
|
||||
subjectCreatedAt = it.createdAt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun senderIntersects(keySet: Set<HexKey>): Boolean = authors.any { it.pubkeyHex in keySet }
|
||||
fun senderIntersects(keySet: Set<HexKey>): Boolean = activeSenders.any { it.pubkeyHex in keySet }
|
||||
|
||||
fun pruneMessagesToTheLatestOnly(): Set<Note> {
|
||||
val sorted = roomMessages.sortedWith(DefaultFeedOrder)
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.privateChats
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
class ChatroomList(
|
||||
val ownerPubKey: HexKey,
|
||||
) {
|
||||
var chatrooms = LargeCache<ChatroomKey, Chatroom>()
|
||||
private set
|
||||
|
||||
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom = chatrooms.getOrCreate(key) { Chatroom() }
|
||||
|
||||
fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
||||
val key = ChatroomKey(persistentSetOf(user.pubkeyHex))
|
||||
return getOrCreatePrivateChatroom(key)
|
||||
}
|
||||
|
||||
fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = getOrCreatePrivateChatroomSync(key)
|
||||
|
||||
fun addMessage(
|
||||
room: ChatroomKey,
|
||||
msg: Note,
|
||||
) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(room)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.addMessageSync(msg)
|
||||
}
|
||||
}
|
||||
|
||||
fun addMessage(
|
||||
user: User,
|
||||
msg: Note,
|
||||
) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.addMessageSync(msg)
|
||||
if (msg.author?.pubkeyHex == ownerPubKey) {
|
||||
privateChatroom.ownerSentMessage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createChatroom(withKey: ChatroomKey) {
|
||||
getOrCreatePrivateChatroom(withKey)
|
||||
}
|
||||
|
||||
fun removeMessage(
|
||||
user: User,
|
||||
msg: Note,
|
||||
) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.removeMessageSync(msg)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMessage(
|
||||
room: ChatroomKey,
|
||||
msg: Note,
|
||||
) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(room)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.removeMessageSync(msg)
|
||||
}
|
||||
}
|
||||
|
||||
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
|
||||
if (key == null) return false
|
||||
return chatrooms.get(key)?.ownerSentMessage == true
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -21,10 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.model.serverList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.GeohashListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HashtagListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlin.collections.map
|
||||
|
||||
class MergedFollowListsState(
|
||||
val kind3List: FollowListState,
|
||||
@@ -45,14 +46,14 @@ class MergedFollowListsState(
|
||||
kind3: FollowListState.Kind3Follows,
|
||||
hashtages: Set<String>,
|
||||
geohashes: Set<String>,
|
||||
community: Set<AddressHint>,
|
||||
community: Set<CommunityTag>,
|
||||
): FollowListState.Kind3Follows =
|
||||
FollowListState.Kind3Follows(
|
||||
kind3.authors,
|
||||
kind3.authorsPlusMe,
|
||||
kind3.hashtags + hashtages,
|
||||
kind3.geotags + geohashes,
|
||||
kind3.communities + community.map { it.addressId },
|
||||
kind3.communities + community.map { it.address.toValue() },
|
||||
)
|
||||
|
||||
val flow: StateFlow<FollowListState.Kind3Follows> =
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model.serverList
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache
|
||||
|
||||
class FeedDecryptionCaches(
|
||||
val peopleListCache: PeopleListDecryptionCache,
|
||||
val muteListCache: MuteListDecryptionCache,
|
||||
val communityListCache: CommunityListDecryptionCache,
|
||||
val hashtagCache: HashtagListDecryptionCache,
|
||||
val geohashCache: GeohashListDecryptionCache,
|
||||
)
|
||||
+2
-1
@@ -51,6 +51,7 @@ class FeedTopNavFilterState(
|
||||
val locationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val caches: FeedDecryptionCaches,
|
||||
val signer: NostrSigner,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
@@ -62,7 +63,7 @@ class FeedTopNavFilterState(
|
||||
else -> {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||
if (note != null) {
|
||||
NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays, blockedRelays)
|
||||
NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays, blockedRelays, caches)
|
||||
} else {
|
||||
UnknownFeedFlow(listName)
|
||||
}
|
||||
|
||||
+21
-44
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased
|
||||
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavFilter
|
||||
@@ -32,11 +33,11 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthors
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -49,30 +50,31 @@ class NoteFeedFlow(
|
||||
val signer: NostrSigner,
|
||||
val allFollowRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val caches: FeedDecryptionCaches,
|
||||
) : IFeedFlowsType {
|
||||
fun process(noteEvent: Event): IFeedTopNavFilter =
|
||||
when (noteEvent) {
|
||||
is PeopleListEvent -> {
|
||||
if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) {
|
||||
MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays)
|
||||
MutedAuthorsByOutboxTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), blockedRelays)
|
||||
} else {
|
||||
AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays)
|
||||
AuthorsByOutboxTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), blockedRelays)
|
||||
}
|
||||
}
|
||||
is MuteListEvent -> {
|
||||
MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users, blockedRelays)
|
||||
MutedAuthorsByOutboxTopNavFilter(caches.muteListCache.cachedUserIdSet(noteEvent), blockedRelays)
|
||||
}
|
||||
is FollowListEvent -> {
|
||||
AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet(), blockedRelays)
|
||||
AuthorsByOutboxTopNavFilter(noteEvent.followIdSet(), blockedRelays)
|
||||
}
|
||||
is CommunityListEvent -> {
|
||||
AllCommunitiesTopNavFilter(noteEvent.publicAndCachedPrivateCommunityIds().toSet(), blockedRelays)
|
||||
AllCommunitiesTopNavFilter(caches.communityListCache.cachedCommunityIdSet(noteEvent), blockedRelays)
|
||||
}
|
||||
is HashtagListEvent -> {
|
||||
HashtagTopNavFilter(noteEvent.publicAndCachedPrivateHashtags(), allFollowRelays)
|
||||
HashtagTopNavFilter(caches.hashtagCache.cachedHashtags(noteEvent), allFollowRelays)
|
||||
}
|
||||
is GeohashListEvent -> {
|
||||
LocationTopNavFilter(noteEvent.publicAndCachedPrivateGeohash(), allFollowRelays)
|
||||
LocationTopNavFilter(caches.geohashCache.cachedGeohashes(noteEvent), allFollowRelays)
|
||||
}
|
||||
is CommunityDefinitionEvent -> {
|
||||
SingleCommunityTopNavFilter(
|
||||
@@ -89,50 +91,25 @@ class NoteFeedFlow(
|
||||
when (noteEvent) {
|
||||
is PeopleListEvent -> {
|
||||
if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays))
|
||||
|
||||
noteEvent.publicAndPrivateUsersAndWords(signer)?.let {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(it.users, blockedRelays))
|
||||
}
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), blockedRelays))
|
||||
} else {
|
||||
emit(AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays))
|
||||
|
||||
noteEvent.publicAndPrivateUsersAndWords(signer)?.let {
|
||||
emit(AuthorsByOutboxTopNavFilter(it.users, blockedRelays))
|
||||
}
|
||||
emit(AuthorsByOutboxTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), blockedRelays))
|
||||
}
|
||||
}
|
||||
is MuteListEvent -> {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users, blockedRelays))
|
||||
|
||||
noteEvent.publicAndPrivateUsersAndWords(signer)?.let {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(it.users, blockedRelays))
|
||||
}
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(caches.muteListCache.mutedUserIdSet(noteEvent), blockedRelays))
|
||||
}
|
||||
is FollowListEvent -> {
|
||||
emit(AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet(), blockedRelays))
|
||||
emit(AuthorsByOutboxTopNavFilter(noteEvent.followIdSet(), blockedRelays))
|
||||
}
|
||||
is CommunityListEvent -> {
|
||||
emit(AllCommunitiesTopNavFilter(noteEvent.publicCommunityIds().toSet(), blockedRelays))
|
||||
|
||||
noteEvent.publicAndPrivateCommunities(signer)?.let {
|
||||
val communities = it.map { it.addressId }.toSet()
|
||||
emit(AllCommunitiesTopNavFilter(communities, blockedRelays))
|
||||
}
|
||||
emit(AllCommunitiesTopNavFilter(caches.communityListCache.communityIdSet(noteEvent), blockedRelays))
|
||||
}
|
||||
is HashtagListEvent -> {
|
||||
emit(HashtagTopNavFilter(noteEvent.publicHashtags().toSet(), allFollowRelays))
|
||||
|
||||
noteEvent.publicAndPrivateHashtag(signer)?.let {
|
||||
emit(HashtagTopNavFilter(it, allFollowRelays))
|
||||
}
|
||||
emit(HashtagTopNavFilter(caches.hashtagCache.hashtags(noteEvent), allFollowRelays))
|
||||
}
|
||||
is GeohashListEvent -> {
|
||||
emit(LocationTopNavFilter(noteEvent.publicGeohashes().toSet(), allFollowRelays))
|
||||
|
||||
noteEvent.publicAndPrivateGeohash(signer)?.let {
|
||||
emit(LocationTopNavFilter(it, allFollowRelays))
|
||||
}
|
||||
emit(LocationTopNavFilter(caches.geohashCache.geohashes(noteEvent), allFollowRelays))
|
||||
}
|
||||
is CommunityDefinitionEvent -> {
|
||||
emit(
|
||||
|
||||
@@ -38,16 +38,13 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.utils.collectSuccessfulOperationsReturning
|
||||
import com.vitorpamplona.quartz.utils.mapNotNullAsync
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.math.round
|
||||
|
||||
class ZapPaymentHandler(
|
||||
@@ -194,7 +191,7 @@ class ZapPaymentHandler(
|
||||
onProgress(0.75f)
|
||||
}
|
||||
|
||||
if (account.hasWalletConnectSetup()) {
|
||||
if (account.nip47SignerState.hasWalletConnectSetup()) {
|
||||
payViaNWC(payables, note, onError = onError, onProgress = {
|
||||
onProgress(it * 0.25f + 0.75f) // keeps within range.
|
||||
}, context)
|
||||
@@ -234,7 +231,14 @@ class ZapPaymentHandler(
|
||||
// makes sure the zap split user receives the zap event
|
||||
val userRelayList = next.user?.inboxRelays()?.toSet() ?: emptySet()
|
||||
|
||||
val zapRequest = prepareZapRequestIfNeeded(note, pollOption, message, zapType, next.user, userRelayList + authorRelayList)
|
||||
val noteEvent = note.event
|
||||
|
||||
val zapRequest =
|
||||
if (zapType != LnZapEvent.ZapType.NONZAP && noteEvent != null) {
|
||||
account.createZapRequestFor(noteEvent, pollOption, message, zapType, next.user, userRelayList + authorRelayList)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
ZapRequestReady(next, zapRequest)
|
||||
}
|
||||
@@ -302,17 +306,12 @@ class ZapPaymentHandler(
|
||||
): List<Paid> {
|
||||
var progressAllPayments = 0.00f
|
||||
|
||||
return collectSuccessfulOperationsReturning(
|
||||
return mapNotNullAsync(
|
||||
items = payables,
|
||||
runRequestFor = { payable: Payable, onReady ->
|
||||
runRequestFor = { payable: Payable ->
|
||||
account.sendZapPaymentRequestFor(
|
||||
bolt11 = payable.invoice,
|
||||
zappedNote = note,
|
||||
onSent = {
|
||||
progressAllPayments += 0.5f / payables.size
|
||||
onProgress(progressAllPayments)
|
||||
onReady(Paid(payable, true))
|
||||
},
|
||||
onResponse = { response ->
|
||||
if (response is PayInvoiceErrorResponse) {
|
||||
progressAllPayments += 0.5f / payables.size
|
||||
@@ -333,6 +332,11 @@ class ZapPaymentHandler(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
progressAllPayments += 0.5f / payables.size
|
||||
onProgress(progressAllPayments)
|
||||
|
||||
Paid(payable, true)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -372,22 +376,4 @@ class ZapPaymentHandler(
|
||||
invoice = invoice,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun prepareZapRequestIfNeeded(
|
||||
note: Note,
|
||||
pollOption: Int?,
|
||||
message: String,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
overrideUser: User? = null,
|
||||
additionalRelays: Set<NormalizedRelayUrl>? = null,
|
||||
): LnZapRequestEvent? =
|
||||
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
||||
tryAndWait { continuation ->
|
||||
account.createZapRequestFor(note, pollOption, message, zapType, overrideUser, additionalRelays) { zapRequest ->
|
||||
continuation.resume(zapRequest)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.Base64
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class V3Parser {
|
||||
companion object Companion {
|
||||
companion object {
|
||||
fun parseCashuA(cashuToken: String): GenericLoadable<ImmutableList<CashuToken>> {
|
||||
try {
|
||||
val base64token = cashuToken.replace("cashuA", "")
|
||||
|
||||
@@ -65,7 +65,6 @@ class V4Parser {
|
||||
|
||||
return GenericLoadable.Loaded(converted.toImmutableList())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
if (e is CancellationException) throw e
|
||||
return GenericLoadable.Error("Could not parse this cashu token")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.location
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.fonfon.kgeohash.GeoHash
|
||||
import com.fonfon.kgeohash.toGeoHash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision
|
||||
@@ -77,7 +78,7 @@ class LocationState(
|
||||
}.onEach {
|
||||
latestLocation = it
|
||||
}.catch { e ->
|
||||
e.printStackTrace()
|
||||
Log.w("GeohashStateFlow", "Exception in the flow", e)
|
||||
latestLocation = LocationResult.LackPermission
|
||||
emit(LocationResult.LackPermission)
|
||||
}
|
||||
|
||||
+1
@@ -84,6 +84,7 @@ class ReverseGeolocation {
|
||||
1,
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.w("ReverseGeolocation", "IO Error", e)
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
|
||||
+79
-91
@@ -42,7 +42,6 @@ import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEven
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
@@ -86,12 +85,11 @@ class EventNotificationConsumer(
|
||||
) {
|
||||
val signer = account.createSigner(applicationContext.contentResolver)
|
||||
|
||||
pushWrappedEvent.unwrapThrowing(signer) { notificationEvent ->
|
||||
consumeNotificationEvent(notificationEvent, signer, account)
|
||||
}
|
||||
val notificationEvent = pushWrappedEvent.unwrapThrowing(signer)
|
||||
consumeNotificationEvent(notificationEvent, signer, account)
|
||||
}
|
||||
|
||||
fun consumeNotificationEvent(
|
||||
suspend fun consumeNotificationEvent(
|
||||
notificationEvent: Event,
|
||||
signer: NostrSigner,
|
||||
account: AccountSettings,
|
||||
@@ -100,22 +98,17 @@ class EventNotificationConsumer(
|
||||
Log.d(TAG, "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${signer.pubKey} consumed= $consumed")
|
||||
if (!consumed) {
|
||||
Log.d(TAG, "New Notification was verified")
|
||||
unwrapAndConsume(notificationEvent, signer) { innerEvent ->
|
||||
if (!notificationManager().areNotificationsEnabled()) return@unwrapAndConsume
|
||||
if (!notificationManager().areNotificationsEnabled()) return
|
||||
Log.d(TAG, "Notifications are enabled")
|
||||
|
||||
Log.d(TAG, "Unwrapped consume $consumed ${innerEvent.javaClass.simpleName}")
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
Log.d(TAG, "New Nip-04 DM to Notify")
|
||||
notify(innerEvent, signer, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
Log.d(TAG, "New Zap to Notify")
|
||||
notify(innerEvent, signer, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
Log.d(TAG, "New ChatMessage to Notify")
|
||||
notify(innerEvent, signer, account)
|
||||
} else if (innerEvent is ChatMessageEncryptedFileHeaderEvent) {
|
||||
Log.d(TAG, "New ChatMessage File to Notify")
|
||||
notify(innerEvent, signer, account)
|
||||
unwrapAndConsume(notificationEvent, signer)?.let { innerEvent ->
|
||||
Log.d(TAG, "Unwrapped consume ${innerEvent.javaClass.simpleName}")
|
||||
|
||||
when (innerEvent) {
|
||||
is PrivateDmEvent -> notify(innerEvent, signer, account)
|
||||
is LnZapEvent -> notify(innerEvent, signer, account)
|
||||
is ChatMessageEvent -> notify(innerEvent, signer, account)
|
||||
is ChatMessageEncryptedFileHeaderEvent -> notify(innerEvent, signer, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,42 +139,45 @@ class EventNotificationConsumer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun unwrapAndConsume(
|
||||
private suspend fun unwrapAndConsume(
|
||||
event: Event,
|
||||
signer: NostrSigner,
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
if (LocalCache.hasConsumed(event)) return
|
||||
): Event? {
|
||||
if (LocalCache.hasConsumed(event)) return null
|
||||
|
||||
when (event) {
|
||||
return when (event) {
|
||||
is GiftWrapEvent -> {
|
||||
if (LocalCache.justConsume(event, null, false)) {
|
||||
// new event
|
||||
event.unwrap(signer) {
|
||||
// clear the encrypted payload to save memory
|
||||
LocalCache.getOrCreateNote(event.id).event = event.copyNoContent()
|
||||
val inner = event.unwrapThrowing(signer)
|
||||
// clear the encrypted payload to save memory
|
||||
LocalCache.getOrCreateNote(event.id).event = event.copyNoContent()
|
||||
|
||||
unwrapAndConsume(it, signer, onReady)
|
||||
}
|
||||
unwrapAndConsume(inner, signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
is SealedRumorEvent -> {
|
||||
if (LocalCache.justConsume(event, null, false)) {
|
||||
// new event
|
||||
event.unseal(signer) {
|
||||
// clear the encrypted payload to save memory
|
||||
LocalCache.getOrCreateNote(event.id).event = event.copyNoContent()
|
||||
val inner = event.unsealThrowing(signer)
|
||||
// clear the encrypted payload to save memory
|
||||
LocalCache.getOrCreateNote(event.id).event = event.copyNoContent()
|
||||
|
||||
// this is not verifiable
|
||||
if (LocalCache.justConsume(it, null, true)) {
|
||||
onReady(it)
|
||||
}
|
||||
// this is not verifiable
|
||||
if (LocalCache.justConsume(inner, null, true)) {
|
||||
event
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
LocalCache.justConsume(event, null, false)
|
||||
onReady(event)
|
||||
event
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,6 +187,7 @@ class EventNotificationConsumer(
|
||||
signer: NostrSigner,
|
||||
acc: AccountSettings,
|
||||
) {
|
||||
Log.d(TAG, "New ChatMessage File to Notify")
|
||||
if (
|
||||
// old event being re-broadcasted
|
||||
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
|
||||
@@ -198,7 +195,7 @@ class EventNotificationConsumer(
|
||||
event.pubKey != signer.pubKey
|
||||
) { // from the user
|
||||
Log.d(TAG, "Notifying")
|
||||
val myUser = LocalCache.getUserIfExists(signer.pubKey) ?: return
|
||||
val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey)
|
||||
val chatNote = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
val chatRoom = event.chatroomKey(signer.pubKey)
|
||||
|
||||
@@ -206,8 +203,7 @@ class EventNotificationConsumer(
|
||||
|
||||
val isKnownRoom =
|
||||
(
|
||||
myUser.privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
|
||||
myUser.hasSentMessagesTo(chatRoom)
|
||||
chatroomList.chatrooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom)
|
||||
)
|
||||
|
||||
if (isKnownRoom) {
|
||||
@@ -236,6 +232,7 @@ class EventNotificationConsumer(
|
||||
signer: NostrSigner,
|
||||
acc: AccountSettings,
|
||||
) {
|
||||
Log.d(TAG, "New ChatMessage to Notify")
|
||||
if (
|
||||
// old event being re-broadcasted
|
||||
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
|
||||
@@ -243,17 +240,13 @@ class EventNotificationConsumer(
|
||||
event.pubKey != signer.pubKey
|
||||
) { // from the user
|
||||
Log.d(TAG, "Notifying")
|
||||
val myUser = LocalCache.getUserIfExists(signer.pubKey) ?: return
|
||||
val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey)
|
||||
val chatNote = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
val chatRoom = event.chatroomKey(signer.pubKey)
|
||||
|
||||
val followingKeySet = acc.backupContactList?.unverifiedFollowKeySet()?.toSet() ?: return
|
||||
|
||||
val isKnownRoom =
|
||||
(
|
||||
myUser.privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
|
||||
myUser.hasSentMessagesTo(chatRoom)
|
||||
)
|
||||
val isKnownRoom = chatroomList.chatrooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom)
|
||||
|
||||
if (isKnownRoom) {
|
||||
val content = chatNote.event?.content ?: ""
|
||||
@@ -274,13 +267,14 @@ class EventNotificationConsumer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(
|
||||
private suspend fun notify(
|
||||
event: PrivateDmEvent,
|
||||
signer: NostrSigner,
|
||||
acc: AccountSettings,
|
||||
) {
|
||||
Log.d(TAG, "New Nip-04 DM to Notify")
|
||||
val note = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
val myUser = LocalCache.getUserIfExists(signer.pubKey) ?: return
|
||||
val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey)
|
||||
|
||||
// old event being re-broadcast
|
||||
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return
|
||||
@@ -290,13 +284,11 @@ class EventNotificationConsumer(
|
||||
|
||||
val chatRoom = event.chatroomKey(signer.pubKey)
|
||||
|
||||
val isKnownRoom =
|
||||
myUser.privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
|
||||
myUser.hasSentMessagesTo(chatRoom)
|
||||
val isKnownRoom = chatroomList.chatrooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom)
|
||||
|
||||
if (isKnownRoom) {
|
||||
note.author?.let {
|
||||
decryptContent(note, signer) { content ->
|
||||
decryptContent(note, signer)?.let { content ->
|
||||
val user = note.author?.toBestDisplayName() ?: ""
|
||||
val userPicture = note.author?.profilePicture()
|
||||
val noteUri = note.toNEvent() + "?account=" + acc.keyPair.pubKey.toNpub()
|
||||
@@ -308,47 +300,44 @@ class EventNotificationConsumer(
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptZapContentAuthor(
|
||||
note: Note,
|
||||
suspend fun decryptZapContentAuthor(
|
||||
event: LnZapRequestEvent,
|
||||
signer: NostrSigner,
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
val event = note.event
|
||||
if (event is LnZapRequestEvent) {
|
||||
if (event.isPrivateZap()) {
|
||||
event.decryptPrivateZap(signer) { onReady(it) }
|
||||
} else {
|
||||
onReady(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptContent(
|
||||
note: Note,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val event = note.event
|
||||
if (event is PrivateDmEvent) {
|
||||
event.plainContent(signer, onReady)
|
||||
} else if (event is LnZapRequestEvent) {
|
||||
decryptZapContentAuthor(note, signer) { onReady(it.content) }
|
||||
} else if (event is DraftEvent) {
|
||||
event.cachedDraft(signer) {
|
||||
onReady(it.content)
|
||||
}
|
||||
): Event? =
|
||||
if (event.isPrivateZap() && event.zappedAuthor().contains(event.pubKey)) {
|
||||
signer.decryptZapEvent(event)
|
||||
} else {
|
||||
event?.content?.let { onReady(it) }
|
||||
event
|
||||
}
|
||||
|
||||
suspend fun decryptContent(
|
||||
note: Note,
|
||||
signer: NostrSigner,
|
||||
): String? {
|
||||
val event = note.event
|
||||
when (event) {
|
||||
is PrivateDmEvent -> {
|
||||
return event.decryptContent(signer)
|
||||
}
|
||||
|
||||
is LnZapRequestEvent -> {
|
||||
return decryptZapContentAuthor(event, signer)?.content
|
||||
}
|
||||
|
||||
else -> {
|
||||
return event?.content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(
|
||||
private suspend fun notify(
|
||||
event: LnZapEvent,
|
||||
signer: NostrSigner,
|
||||
acc: AccountSettings,
|
||||
) {
|
||||
Log.d(TAG, "New Zap to Notify")
|
||||
Log.d(TAG, "Notify Start ${event.toNostrUri()}")
|
||||
val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
LocalCache.getNoteIfExists(event.id) ?: return
|
||||
|
||||
Log.d(TAG, "Notify Not Notified Yet")
|
||||
|
||||
@@ -358,8 +347,7 @@ class EventNotificationConsumer(
|
||||
Log.d(TAG, "Notify Not an old event")
|
||||
|
||||
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
val noteZapped =
|
||||
event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
|
||||
Log.d(TAG, "Notify ZapRequest $noteZapRequest zapped $noteZapped")
|
||||
|
||||
@@ -373,17 +361,17 @@ class EventNotificationConsumer(
|
||||
Log.d(TAG, "Notify Amount $amount")
|
||||
|
||||
(noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
|
||||
decryptZapContentAuthor(noteZapRequest, signer) {
|
||||
decryptZapContentAuthor(event, signer)?.let { decryptedEvent ->
|
||||
Log.d(TAG, "Notify Decrypted if Private Zap ${event.id}")
|
||||
|
||||
val author = LocalCache.getOrCreateUser(it.pubKey)
|
||||
val senderInfo = Pair(author, it.content.ifBlank { null })
|
||||
val author = LocalCache.getOrCreateUser(decryptedEvent.pubKey)
|
||||
val senderInfo = Pair(author, decryptedEvent.content.ifBlank { null })
|
||||
|
||||
if (noteZapped.event?.content != null) {
|
||||
decryptContent(noteZapped, signer) {
|
||||
decryptContent(noteZapped, signer)?.let { decrypted ->
|
||||
Log.d(TAG, "Notify Decrypted if Private Note")
|
||||
|
||||
val zappedContent = it.split("\n").get(0)
|
||||
val zappedContent = decrypted.split("\n")[0]
|
||||
|
||||
val user = senderInfo.first.toBestDisplayName()
|
||||
var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
|
||||
|
||||
+2
-8
@@ -30,7 +30,6 @@ import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.utils.mapNotNullAsync
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -38,7 +37,6 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.coroutines.executeAsync
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class RegisterAccounts(
|
||||
private val accounts: List<AccountInfo>,
|
||||
@@ -61,12 +59,8 @@ class RegisterAccounts(
|
||||
}
|
||||
|
||||
return mapNotNullAsync(remainingTos) { info ->
|
||||
tryAndWait { continuation ->
|
||||
val signer = info.accountSettings.createSigner(Amethyst.instance.contentResolver)
|
||||
RelayAuthEvent.create(info.relays, notificationToken, signer) { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
val signer = info.accountSettings.createSigner(Amethyst.instance.contentResolver)
|
||||
RelayAuthEvent.create(info.relays, notificationToken, signer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayAuthenticator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class ScreenAuthAccount(
|
||||
val account: Account,
|
||||
@@ -32,11 +33,12 @@ class ScreenAuthAccount(
|
||||
|
||||
class AuthCoordinator(
|
||||
client: NostrClient,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
private val authWithAccounts = ListWithUniqueSetCache<ScreenAuthAccount, Account> { it.account }
|
||||
|
||||
val receiver =
|
||||
RelayAuthenticator(client) { challenge, relay ->
|
||||
RelayAuthenticator(client, scope) { challenge, relay ->
|
||||
authWithAccounts.distinct().forEach {
|
||||
it.sendAuthEvent(relay, challenge)
|
||||
}
|
||||
|
||||
+3
-3
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState.Companion.APP_SPECIFIC_DATA_D_TAG
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
@@ -29,10 +28,11 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
|
||||
+2
-2
@@ -28,8 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
|
||||
val ReportsAndBookmarksFromKeyKinds =
|
||||
|
||||
+3
-3
@@ -27,9 +27,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
|
||||
|
||||
val FollowAndMutesFromKeyKinds =
|
||||
|
||||
+23
-42
@@ -29,14 +29,15 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -277,9 +278,9 @@ fun observeUserTagFollowCount(
|
||||
.metadata.stateFlow
|
||||
.sample(1000)
|
||||
.mapLatest { noteState ->
|
||||
(noteState.note.event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.size ?: 0
|
||||
(noteState.note.event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0
|
||||
}.onStart {
|
||||
emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.size ?: 0)
|
||||
emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
@@ -305,9 +306,9 @@ fun observeUserTagFollows(
|
||||
.metadata.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { noteState ->
|
||||
(noteState.note.event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.sorted() ?: emptyList()
|
||||
(noteState.note.event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList()
|
||||
}.onStart {
|
||||
emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.sorted() ?: emptyList())
|
||||
emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList())
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
@@ -319,15 +320,21 @@ fun observeUserTagFollows(
|
||||
fun observeUserBookmarks(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<UserState?> {
|
||||
): State<NoteState> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user
|
||||
.flow()
|
||||
.bookmarks.stateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
val flow =
|
||||
remember(user) {
|
||||
accountViewModel
|
||||
.bookmarks(user)
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@@ -342,12 +349,13 @@ fun observeUserBookmarkCount(
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
accountViewModel
|
||||
.bookmarks(user)
|
||||
.flow()
|
||||
.followers.stateFlow
|
||||
.metadata.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { userState ->
|
||||
userState.user.latestBookmarkList?.countBookmarks() ?: 0
|
||||
.mapLatest { noteState ->
|
||||
(noteState.note.event as? BookmarkListEvent)?.countBookmarks() ?: 0
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
@@ -661,33 +669,6 @@ fun observeUserRelayIntoList(
|
||||
return flow.collectAsStateWithLifecycle(false)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeUserRoomSubject(
|
||||
user: User,
|
||||
room: ChatroomKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<String?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.flow()
|
||||
.messages
|
||||
.stateFlow
|
||||
.sample(1000)
|
||||
.mapLatest { userState ->
|
||||
userState.user.privateChatrooms[room]?.subject
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(user.privateChatrooms[room]?.subject)
|
||||
}
|
||||
|
||||
data class RelayUsage(
|
||||
val relays: List<NormalizedRelayUrl> = emptyList(),
|
||||
val userRelayList: List<NormalizedRelayUrl> = emptyList(),
|
||||
|
||||
+3
-3
@@ -36,10 +36,10 @@ 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.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.speedLogger
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger.Companion.TAG
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.concurrent.timer
|
||||
|
||||
class FrameStat {
|
||||
var eventCount = AtomicInteger(0)
|
||||
var kinds = LargeCache<Int, KindGroup>()
|
||||
|
||||
fun increment(
|
||||
kind: Int,
|
||||
subId: String,
|
||||
relayUrl: NormalizedRelayUrl,
|
||||
memory: Long,
|
||||
) {
|
||||
eventCount.incrementAndGet()
|
||||
|
||||
val kindGroup = kinds.get(kind)
|
||||
if (kindGroup != null) {
|
||||
kindGroup.increment(memory, subId, relayUrl)
|
||||
} else {
|
||||
val group = KindGroup()
|
||||
group.increment(memory, subId, relayUrl)
|
||||
kinds.put(kind, group)
|
||||
}
|
||||
}
|
||||
|
||||
fun hasAnything() = eventCount.get() > 0
|
||||
|
||||
fun reset() {
|
||||
eventCount.set(0)
|
||||
kinds.forEach { key, value -> value.reset() }
|
||||
}
|
||||
|
||||
fun log() {
|
||||
Log.d(TAG, "Events Per Second: ${eventCount.get()}")
|
||||
kinds.forEach { key, value ->
|
||||
if (value.count.get() > 0) {
|
||||
Log.d(TAG, "-- Kind $key $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// Use a timer to reset the counter every second.
|
||||
timer(name = "EventsPerSecondCounter", period = 1000, daemon = true) {
|
||||
if (hasAnything()) {
|
||||
log()
|
||||
reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-97
@@ -18,21 +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.relayClient
|
||||
package com.vitorpamplona.amethyst.service.relayClient.speedLogger
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.service.relayClient.RelaySpeedLogger.Companion.TAG
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.concurrent.timer
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
class KindGroup(
|
||||
@@ -81,92 +74,3 @@ class KindGroup(
|
||||
|
||||
override fun toString() = "(${count.get()} - ${memory.get().div(MB)}kb); ${printSubs()}; ${printRelays()}"
|
||||
}
|
||||
|
||||
class FrameStat {
|
||||
var eventCount = AtomicInteger(0)
|
||||
var kinds = LargeCache<Int, KindGroup>()
|
||||
|
||||
fun increment(
|
||||
kind: Int,
|
||||
subId: String,
|
||||
relayUrl: NormalizedRelayUrl,
|
||||
memory: Long,
|
||||
) {
|
||||
eventCount.incrementAndGet()
|
||||
|
||||
val kindGroup = kinds.get(kind)
|
||||
if (kindGroup != null) {
|
||||
kindGroup.increment(memory, subId, relayUrl)
|
||||
} else {
|
||||
val group = KindGroup()
|
||||
group.increment(memory, subId, relayUrl)
|
||||
kinds.put(kind, group)
|
||||
}
|
||||
}
|
||||
|
||||
fun hasAnything() = eventCount.get() > 0
|
||||
|
||||
fun reset() {
|
||||
eventCount.set(0)
|
||||
kinds.forEach { key, value -> value.reset() }
|
||||
}
|
||||
|
||||
fun log() {
|
||||
Log.d(TAG, "Events Per Second: ${eventCount.get()}")
|
||||
kinds.forEach { key, value ->
|
||||
if (value.count.get() > 0) {
|
||||
Log.d(TAG, "-- Kind $key $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the timer in the constructor. This ensures it starts when the
|
||||
// ResettableCounter object is created.
|
||||
init {
|
||||
// Use a timer to reset the counter every second.
|
||||
timer(name = "EventsPerSecondCounter", period = 1000, daemon = true) {
|
||||
if (hasAnything()) {
|
||||
log()
|
||||
reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to NostrClient's onNotify messages from the relay
|
||||
*/
|
||||
class RelaySpeedLogger(
|
||||
val client: NostrClient,
|
||||
) {
|
||||
companion object {
|
||||
val TAG = RelaySpeedLogger::class.java.simpleName
|
||||
}
|
||||
|
||||
var current = FrameStat()
|
||||
|
||||
private val clientListener =
|
||||
object : IRelayClientListener {
|
||||
/** A new message was received */
|
||||
override fun onEvent(
|
||||
relay: IRelayClient,
|
||||
subId: String,
|
||||
event: Event,
|
||||
arrivalTime: Long,
|
||||
afterEOSE: Boolean,
|
||||
) {
|
||||
current.increment(event.kind, subId, relay.url, event.countMemory())
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d(TAG, "Init, Subscribe")
|
||||
client.subscribe(clientListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
// makes sure to run
|
||||
Log.d(TAG, "Destroy, Unsubscribe")
|
||||
client.unsubscribe(clientListener)
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.speedLogger
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger.Companion.TAG
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
|
||||
/**
|
||||
* Listens to NostrClient's onNotify messages from the relay
|
||||
*/
|
||||
class RelaySpeedLogger(
|
||||
val client: NostrClient,
|
||||
) {
|
||||
companion object {
|
||||
val TAG = RelaySpeedLogger::class.java.simpleName
|
||||
}
|
||||
|
||||
var current = FrameStat()
|
||||
|
||||
private val clientListener =
|
||||
object : IRelayClientListener {
|
||||
/** A new message was received */
|
||||
override fun onEvent(
|
||||
relay: IRelayClient,
|
||||
subId: String,
|
||||
event: Event,
|
||||
arrivalTime: Long,
|
||||
afterEOSE: Boolean,
|
||||
) {
|
||||
current.increment(event.kind, subId, relay.url, event.countMemory())
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d(TAG, "Init, Subscribe")
|
||||
client.subscribe(clientListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
// makes sure to run
|
||||
Log.d(TAG, "Destroy, Unsubscribe")
|
||||
client.unsubscribe(clientListener)
|
||||
}
|
||||
}
|
||||
+36
-35
@@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -47,7 +47,6 @@ class MultiOrchestrator(
|
||||
fun first() = list.first()
|
||||
|
||||
suspend fun upload(
|
||||
scope: CoroutineScope,
|
||||
alt: String?,
|
||||
contentWarningReason: String?,
|
||||
mediaQuality: CompressorQuality,
|
||||
@@ -55,29 +54,30 @@ class MultiOrchestrator(
|
||||
account: Account,
|
||||
context: Context,
|
||||
): Result {
|
||||
val jobs =
|
||||
list.map { item ->
|
||||
scope.launch(Dispatchers.IO) {
|
||||
item.orchestrator.upload(
|
||||
item.media.uri,
|
||||
item.media.mimeType,
|
||||
alt,
|
||||
contentWarningReason,
|
||||
mediaQuality,
|
||||
server,
|
||||
account,
|
||||
context,
|
||||
)
|
||||
coroutineScope {
|
||||
val jobs =
|
||||
list.map { item ->
|
||||
launch(Dispatchers.IO) {
|
||||
item.orchestrator.upload(
|
||||
item.media.uri,
|
||||
item.media.mimeType,
|
||||
alt,
|
||||
contentWarningReason,
|
||||
mediaQuality,
|
||||
server,
|
||||
account,
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jobs.joinAll()
|
||||
jobs.joinAll()
|
||||
}
|
||||
|
||||
return computeFinalResults()
|
||||
}
|
||||
|
||||
suspend fun uploadEncrypted(
|
||||
scope: CoroutineScope,
|
||||
alt: String?,
|
||||
contentWarningReason: String?,
|
||||
mediaQuality: CompressorQuality,
|
||||
@@ -86,24 +86,25 @@ class MultiOrchestrator(
|
||||
account: Account,
|
||||
context: Context,
|
||||
): Result {
|
||||
val jobs =
|
||||
list.map { item ->
|
||||
scope.launch(Dispatchers.IO) {
|
||||
item.orchestrator.uploadEncrypted(
|
||||
item.media.uri,
|
||||
item.media.mimeType,
|
||||
alt,
|
||||
contentWarningReason,
|
||||
mediaQuality,
|
||||
cipher,
|
||||
server,
|
||||
account,
|
||||
context,
|
||||
)
|
||||
coroutineScope {
|
||||
val jobs =
|
||||
list.map { item ->
|
||||
launch(Dispatchers.IO) {
|
||||
item.orchestrator.uploadEncrypted(
|
||||
item.media.uri,
|
||||
item.media.mimeType,
|
||||
alt,
|
||||
contentWarningReason,
|
||||
mediaQuality,
|
||||
cipher,
|
||||
server,
|
||||
account,
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jobs.joinAll()
|
||||
jobs.joinAll()
|
||||
}
|
||||
|
||||
return computeFinalResults()
|
||||
}
|
||||
|
||||
+5
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -166,6 +167,8 @@ class UploadOrchestrator {
|
||||
originalHash = originalHash,
|
||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
|
||||
)
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
error(R.string.login_with_a_private_key_to_be_able_to_upload)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
error(R.string.failed_to_upload_media, e.message ?: e.javaClass.simpleName)
|
||||
@@ -207,6 +210,8 @@ class UploadOrchestrator {
|
||||
originalHash = originalHash,
|
||||
originalContentType = contentTypeForResult,
|
||||
)
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
error(R.string.login_with_a_private_key_to_be_able_to_upload)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
error(R.string.failed_to_upload_media, e.message ?: e.javaClass.simpleName)
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ class ServerInfoRetriever {
|
||||
val body = response.body.string()
|
||||
parser.parse(baseUrl, body)
|
||||
} else {
|
||||
throw RuntimeException(
|
||||
throw Exception(
|
||||
"Resulting Message from $baseUrl is an error: ${response.code} ${response.message}",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -163,14 +163,14 @@ fun uriToRoute(
|
||||
is NEvent -> {
|
||||
routeFor(
|
||||
note = LocalCache.getOrCreateNote(nip19.hex),
|
||||
loggedIn = account.userProfile(),
|
||||
loggedIn = account,
|
||||
) ?: Route.EventRedirect(nip19.hex)
|
||||
}
|
||||
|
||||
is NAddress -> {
|
||||
routeFor(
|
||||
note = LocalCache.getOrCreateAddressableNote(nip19.address()),
|
||||
loggedIn = account.userProfile(),
|
||||
loggedIn = account,
|
||||
) ?: Route.EventRedirect(nip19.aTag())
|
||||
}
|
||||
|
||||
@@ -179,12 +179,12 @@ fun uriToRoute(
|
||||
if (noteEvent is AddressableEvent) {
|
||||
routeFor(
|
||||
note = LocalCache.getOrCreateAddressableNote(noteEvent.address()),
|
||||
loggedIn = account.userProfile(),
|
||||
loggedIn = account,
|
||||
) ?: Route.EventRedirect(noteEvent.addressTag())
|
||||
} else {
|
||||
routeFor(
|
||||
note = LocalCache.getOrCreateNote(nip19.event.id),
|
||||
loggedIn = account.userProfile(),
|
||||
loggedIn = account,
|
||||
) ?: Route.EventRedirect(nip19.event.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.alt
|
||||
@@ -168,6 +170,23 @@ open class EditPostViewModel : ViewModel() {
|
||||
server: ServerName,
|
||||
onError: (String, String) -> Unit,
|
||||
context: Context,
|
||||
) = try {
|
||||
uploadUnsafe(alt, sensitiveContent, mediaQuality, isPrivate, server, onError, context)
|
||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||
onError(
|
||||
stringRes(context, R.string.read_only_user),
|
||||
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events),
|
||||
)
|
||||
}
|
||||
|
||||
fun uploadUnsafe(
|
||||
alt: String?,
|
||||
sensitiveContent: Boolean,
|
||||
mediaQuality: Int,
|
||||
isPrivate: Boolean = false,
|
||||
server: ServerName,
|
||||
onError: (String, String) -> Unit,
|
||||
context: Context,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val myAccount = account ?: return@launch
|
||||
@@ -177,7 +196,6 @@ open class EditPostViewModel : ViewModel() {
|
||||
|
||||
val results =
|
||||
myMultiOrchestrator.upload(
|
||||
viewModelScope,
|
||||
alt,
|
||||
if (sensitiveContent) "" else null,
|
||||
MediaCompressor.intToCompressorQuality(mediaQuality),
|
||||
@@ -189,21 +207,21 @@ open class EditPostViewModel : ViewModel() {
|
||||
if (results.allGood) {
|
||||
results.successful.forEach { state ->
|
||||
if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
|
||||
account?.createNip95(
|
||||
state.result.bytes,
|
||||
headerInfo = state.result.fileHeader,
|
||||
alt,
|
||||
if (sensitiveContent) "" else null,
|
||||
) { nip95 ->
|
||||
nip95attachments = nip95attachments + nip95
|
||||
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
|
||||
val nip95 =
|
||||
myAccount.createNip95(
|
||||
byteArray = state.result.bytes,
|
||||
headerInfo = state.result.fileHeader,
|
||||
alt = alt,
|
||||
contentWarningReason = if (sensitiveContent) "" else null,
|
||||
)
|
||||
nip95attachments = nip95attachments + nip95
|
||||
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
val iMeta =
|
||||
IMetaTagBuilder(state.result.url)
|
||||
|
||||
@@ -171,7 +171,7 @@ object MediaSaverToDisk {
|
||||
onSuccess()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
Log.w("MediaSaverToDisk", "Unable to save", e)
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,11 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Stable
|
||||
open class NewMediaModel : ViewModel() {
|
||||
@@ -83,6 +81,19 @@ open class NewMediaModel : ViewModel() {
|
||||
context: Context,
|
||||
onSucess: () -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) = try {
|
||||
uploadUnsafe(context, onSucess, onError)
|
||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||
onError(
|
||||
stringRes(context, R.string.read_only_user),
|
||||
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events),
|
||||
)
|
||||
}
|
||||
|
||||
fun uploadUnsafe(
|
||||
context: Context,
|
||||
onSucess: () -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val myAccount = account ?: return@launch
|
||||
@@ -94,7 +105,6 @@ open class NewMediaModel : ViewModel() {
|
||||
|
||||
val results =
|
||||
myMultiOrchestrator.upload(
|
||||
viewModelScope,
|
||||
caption,
|
||||
if (sensitiveContent) "" else null,
|
||||
MediaCompressor.intToCompressorQuality(mediaQualitySlider),
|
||||
@@ -135,14 +145,8 @@ open class NewMediaModel : ViewModel() {
|
||||
nip95s.map {
|
||||
// upload each file as an individual nip95 event.
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withTimeoutOrNull(30000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
account?.createNip95(it.bytes, headerInfo = it.fileHeader, caption, if (sensitiveContent) "" else null) { nip95 ->
|
||||
account?.consumeAndSendNip95(nip95.first, nip95.second)
|
||||
continuation.resume(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
val nip95 = myAccount.createNip95(it.bytes, headerInfo = it.fileHeader, caption, if (sensitiveContent) "" else null)
|
||||
myAccount.consumeAndSendNip95(nip95.first, nip95.second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,20 +154,14 @@ open class NewMediaModel : ViewModel() {
|
||||
videosAndOthers.map {
|
||||
// upload each file as an individual nip95 event.
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withTimeoutOrNull(30000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
account?.sendHeader(
|
||||
url = it.url,
|
||||
magnetUri = it.magnet,
|
||||
headerInfo = it.fileHeader,
|
||||
alt = caption,
|
||||
contentWarningReason = if (sensitiveContent) "" else null,
|
||||
originalHash = it.uploadedHash,
|
||||
) {
|
||||
continuation.resume(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
account?.sendHeader(
|
||||
url = it.url,
|
||||
magnetUri = it.magnet,
|
||||
headerInfo = it.fileHeader,
|
||||
alt = caption,
|
||||
contentWarningReason = if (sensitiveContent) "" else null,
|
||||
originalHash = it.uploadedHash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,17 +169,11 @@ open class NewMediaModel : ViewModel() {
|
||||
if (imageUrls.isNotEmpty()) {
|
||||
listOf(
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withTimeoutOrNull(30000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
account?.sendAllAsOnePictureEvent(
|
||||
urlHeaderInfo = imageUrls,
|
||||
caption = caption,
|
||||
contentWarningReason = if (sensitiveContent) "" else null,
|
||||
) {
|
||||
continuation.resume(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
account?.sendAllAsOnePictureEvent(
|
||||
urlHeaderInfo = imageUrls,
|
||||
caption = caption,
|
||||
contentWarningReason = if (sensitiveContent) "" else null,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
|
||||
+4
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
|
||||
@@ -212,6 +213,9 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
onUploading(false)
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
|
||||
}
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
onUploading(false)
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onUploading(false)
|
||||
|
||||
@@ -183,7 +183,7 @@ private fun DisplayNoteLink(
|
||||
val noteState by observeNote(it, accountViewModel)
|
||||
val noteIdDisplayNote = remember(noteState) { "@${noteState.note.idDisplayNote()}" }
|
||||
|
||||
val route = routeFor(it, accountViewModel.userProfile()) ?: Route.EventRedirect(hex)
|
||||
val route = routeFor(it, accountViewModel.account) ?: Route.EventRedirect(hex)
|
||||
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
|
||||
@@ -767,7 +767,7 @@ private fun DisplayNoteFromTag(
|
||||
} else {
|
||||
ClickableTextPrimary(
|
||||
text = "@${baseNote.idNote().toShortDisplay()}",
|
||||
onClick = { routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } },
|
||||
onClick = { routeFor(baseNote, accountViewModel.account)?.let { nav.nav(it) } },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class FilterByListParams(
|
||||
|
||||
@@ -142,7 +142,7 @@ fun AppNavigation(
|
||||
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||
|
||||
composableFromEndArgs<Route.Room> { ChatroomScreen(it.id.toString(), it.message, it.replyId, it.draftId, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
|
||||
|
||||
composableFromEndArgs<Route.PublicChatChannel> { PublicChatChannelScreen(it.id, accountViewModel, nav) }
|
||||
|
||||
+20
-19
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.navigation.routes
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -50,7 +51,7 @@ import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
|
||||
fun routeFor(
|
||||
note: Note,
|
||||
loggedIn: User,
|
||||
loggedIn: Account,
|
||||
): Route? {
|
||||
val noteEvent = note.event ?: return Route.EventRedirect(note.idHex)
|
||||
|
||||
@@ -59,10 +60,10 @@ fun routeFor(
|
||||
|
||||
fun routeFor(
|
||||
noteEvent: Event,
|
||||
loggedIn: User,
|
||||
loggedIn: Account,
|
||||
): Route? {
|
||||
if (noteEvent is DraftEvent) {
|
||||
val innerEvent = noteEvent.preCachedDraft(loggedIn.pubkeyHex)
|
||||
val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent)
|
||||
|
||||
if (innerEvent is IsInPublicChatChannel) {
|
||||
innerEvent.channelId()?.let {
|
||||
@@ -77,9 +78,9 @@ fun routeFor(
|
||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||
}
|
||||
} else if (innerEvent is ChatroomKeyable) {
|
||||
val room = innerEvent.chatroomKey(loggedIn.pubkeyHex)
|
||||
loggedIn.createChatroom(room)
|
||||
return Route.Room(room.hashCode())
|
||||
val room = innerEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
|
||||
loggedIn.chatroomList.createChatroom(room)
|
||||
return Route.Room(room)
|
||||
} else if (innerEvent is AddressableEvent) {
|
||||
return Route.Note(noteEvent.aTag().toTag())
|
||||
} else {
|
||||
@@ -102,9 +103,9 @@ fun routeFor(
|
||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||
}
|
||||
} else if (noteEvent is ChatroomKeyable) {
|
||||
val room = noteEvent.chatroomKey(loggedIn.pubkeyHex)
|
||||
loggedIn.createChatroom(room)
|
||||
return Route.Room(room.hashCode())
|
||||
val room = noteEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
|
||||
loggedIn.chatroomList.createChatroom(room)
|
||||
return Route.Room(room)
|
||||
} else if (noteEvent is CommunityDefinitionEvent) {
|
||||
return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag())
|
||||
} else if (noteEvent is GiftWrapEvent) {
|
||||
@@ -159,18 +160,18 @@ fun routeToMessage(
|
||||
replyId: HexKey? = null,
|
||||
draftId: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
): Route = routeToMessage(room, draftMessage, replyId, draftId, accountViewModel.userProfile())
|
||||
): Route = routeToMessage(room, draftMessage, replyId, draftId, accountViewModel.account)
|
||||
|
||||
fun routeToMessage(
|
||||
room: ChatroomKey,
|
||||
draftMessage: String?,
|
||||
draftMessage: String? = null,
|
||||
replyId: HexKey? = null,
|
||||
draftId: HexKey? = null,
|
||||
fromUser: User,
|
||||
account: Account,
|
||||
): Route {
|
||||
fromUser.createChatroom(room)
|
||||
account.chatroomList.createChatroom(room)
|
||||
|
||||
return Route.Room(room.hashCode(), draftMessage, replyId, draftId)
|
||||
return Route.Room(room, draftMessage, replyId, draftId)
|
||||
}
|
||||
|
||||
fun routeToMessage(
|
||||
@@ -195,26 +196,26 @@ fun authorRouteFor(note: Note): Route.Profile? = note.author?.pubkeyHex?.let { R
|
||||
|
||||
fun routeReplyTo(
|
||||
note: Note,
|
||||
asUser: User,
|
||||
account: Account,
|
||||
): Route? {
|
||||
val noteEvent = note.event
|
||||
return when (noteEvent) {
|
||||
is TextNoteEvent -> Route.NewPost(baseReplyTo = note.idHex)
|
||||
is PrivateDmEvent ->
|
||||
routeToMessage(
|
||||
room = noteEvent.chatroomKey(asUser.pubkeyHex),
|
||||
room = noteEvent.chatroomKey(account.userProfile().pubkeyHex),
|
||||
draftMessage = null,
|
||||
replyId = noteEvent.id,
|
||||
draftId = null,
|
||||
fromUser = asUser,
|
||||
account = account,
|
||||
)
|
||||
is ChatroomKeyable ->
|
||||
routeToMessage(
|
||||
room = noteEvent.chatroomKey(asUser.pubkeyHex),
|
||||
room = noteEvent.chatroomKey(account.userProfile().pubkeyHex),
|
||||
draftMessage = null,
|
||||
replyId = noteEvent.id,
|
||||
draftId = null,
|
||||
fromUser = asUser,
|
||||
account = account,
|
||||
)
|
||||
is CommentEvent -> {
|
||||
if (noteEvent.isGeohashedScoped()) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.toRoute
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed class Route {
|
||||
@@ -122,11 +123,20 @@ sealed class Route {
|
||||
) : Route()
|
||||
|
||||
@Serializable data class Room(
|
||||
val id: Int,
|
||||
val id: String,
|
||||
val message: String? = null,
|
||||
val replyId: HexKey? = null,
|
||||
val draftId: HexKey? = null,
|
||||
) : Route()
|
||||
) : Route() {
|
||||
constructor(key: ChatroomKey, message: String? = null, replyId: HexKey? = null, draftId: HexKey? = null) : this(
|
||||
id = key.users.joinToString(","),
|
||||
message = message,
|
||||
replyId = replyId,
|
||||
draftId = draftId,
|
||||
)
|
||||
|
||||
fun toKey(): ChatroomKey = ChatroomKey(id.split(",").toSet())
|
||||
}
|
||||
|
||||
@Serializable data class RoomByAuthor(
|
||||
val id: String,
|
||||
|
||||
+3
-3
@@ -71,8 +71,8 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@@ -263,7 +263,7 @@ fun RenderOption(
|
||||
if (noteEvent is PeopleListEvent) {
|
||||
noteEvent.nameOrTitle() ?: option.note.dTag()
|
||||
} else if (noteEvent is FollowListEvent) {
|
||||
noteEvent.nameOrTitle() ?: option.note.dTag()
|
||||
noteEvent.title() ?: option.note.dTag()
|
||||
} else {
|
||||
option.note.dTag()
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ fun BadgeCompose(
|
||||
onClick = {
|
||||
routeFor(
|
||||
note,
|
||||
accountViewModel.userProfile(),
|
||||
accountViewModel.account,
|
||||
)?.let { nav.nav(it) }
|
||||
},
|
||||
),
|
||||
|
||||
@@ -85,7 +85,7 @@ fun MessageSetCompose(
|
||||
scope.launch {
|
||||
routeFor(
|
||||
baseNote,
|
||||
accountViewModel.userProfile(),
|
||||
accountViewModel.account,
|
||||
)?.let { nav.nav(it) }
|
||||
}
|
||||
},
|
||||
|
||||
@@ -134,7 +134,7 @@ fun MultiSetCompose(
|
||||
.background(backgroundColor.value)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch { routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } }
|
||||
scope.launch { routeFor(baseNote, accountViewModel.account)?.let { nav.nav(it) } }
|
||||
},
|
||||
onLongClick = { popupExpanded.value = true },
|
||||
).padding(
|
||||
|
||||
+2
-2
@@ -327,7 +327,7 @@ fun DisplayStatusInner(
|
||||
onClick = {
|
||||
routeFor(
|
||||
note,
|
||||
accountViewModel.userProfile(),
|
||||
accountViewModel.account,
|
||||
)?.let { nav.nav(it) }
|
||||
},
|
||||
) {
|
||||
@@ -349,7 +349,7 @@ fun DisplayStatusInner(
|
||||
onClick = {
|
||||
routeFor(
|
||||
it,
|
||||
accountViewModel.userProfile(),
|
||||
accountViewModel.account,
|
||||
)?.let { nav.nav(it) }
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -183,12 +183,12 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
@@ -474,7 +474,7 @@ fun ClickableNote(
|
||||
} else {
|
||||
baseNote
|
||||
}
|
||||
routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav.nav(it) }
|
||||
routeFor(redirectToNote, accountViewModel.account)?.let { nav.nav(it) }
|
||||
},
|
||||
onLongClick = showPopup,
|
||||
).background(backgroundColor.value)
|
||||
|
||||
@@ -94,7 +94,7 @@ 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.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user