Fix merge-related conflicts.

This commit is contained in:
KotlinGeekDev
2025-03-08 00:49:18 +01:00
900 changed files with 35580 additions and 14413 deletions
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
import junit.framework.TestCase.assertEquals
import okhttp3.Request
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DMDecryptionTest {
val okHttp = HttpClientManager.getHttpClient(false)
val url = "https://cdn.satellite.earth/812fd4cf9d4d4b59c141ecd6a6c08c7571b5872237ad6477916cb2d119b5cacd"
val cipher =
AESGCM(
"7b184b3849e161027bab1aac9f2d96eb22bfe02c8dc34cad5d2cc436a64f191d".hexToByteArray(),
"3936923383652e3f0d72bfd40568b435".hexToByteArray(),
)
val decryptedSize = 1277122
val expectedMimeType = "video/mp4"
@Test
fun runDownloadAndDecryptVideo() {
HttpClientManager.addCipherToCache(url, cipher, "video/mp4")
val request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.get()
.build()
okHttp.newCall(request).execute().use {
assertEquals(decryptedSize, it.body.bytes().size)
assertEquals(expectedMimeType, it.body.contentType().toString())
}
}
}
@@ -34,9 +34,9 @@ import com.vitorpamplona.amethyst.service.uploads.nip96.ServerInfoRetriever
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.utils.sha256.sha256
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.fail
import kotlinx.coroutines.CoroutineScope
@@ -81,7 +81,7 @@ class ImageUploadTesting {
private suspend fun testBlossom(server: ServerName) {
val paylod = getBitmap()
val initialHash = CryptoUtils.sha256(paylod).toHexKey()
val initialHash = sha256(paylod).toHexKey()
val inputStream = paylod.inputStream()
val result =
BlossomUploader()
@@ -111,7 +111,7 @@ class ImageUploadTesting {
return
}
val downloadedHash = CryptoUtils.sha256(imageData).toHexKey()
val downloadedHash = sha256(imageData).toHexKey()
assertEquals(initialHash, downloadedHash)
}
@@ -23,10 +23,11 @@ package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps
import junit.framework.TestCase.assertEquals
import org.junit.Assert
@@ -46,27 +47,27 @@ class OkHttpOtsTest {
@Before
fun setup() {
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(forceProxy = { false }), OkHttpCalendarBuilder(forceProxy = { false }))
OtsResolver.ots = OpenTimestamps(OkHttpBlockstreamExplorer(forceProxy = { false }), OkHttpCalendarBuilder(forceProxy = { false }))
}
@Test
fun verifyNostrEvent() {
val ots = EventMapper.fromJson(otsEvent) as OtsEvent
println(ots.info())
println(OtsResolver.info(ots.otsByteArray()))
assertEquals(1707688818L, ots.verify())
}
@Test
fun verifyNostrEvent2() {
val ots = EventMapper.fromJson(otsEvent2) as OtsEvent
println(ots.info())
println(OtsResolver.info(ots.otsByteArray()))
assertEquals(1706322179L, ots.verify())
}
@Test
fun verifyNostrPendingEvent() {
val ots = EventMapper.fromJson(otsPendingEvent) as OtsEvent
println(ots.info())
println(OtsResolver.info(ots.otsByteArray()))
assertEquals(null, ots.verify())
}
@@ -79,7 +80,7 @@ class OkHttpOtsTest {
val otsFile = OtsEvent.stamp(otsEvent2Digest)
OtsEvent.create(otsEvent2Digest, otsFile, signer) {
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile)) {
ots = it
countDownLatch.countDown()
}
@@ -87,7 +88,7 @@ class OkHttpOtsTest {
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
println(ots!!.toJson())
println(ots!!.info())
println(OtsResolver.info(ots!!.otsByteArray()))
// Should not be valid because we need to wait for confirmations
assertEquals(null, ots!!.verify())
@@ -26,11 +26,11 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.hasValidSignature
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.verify
import junit.framework.TestCase
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.CoroutineScope
@@ -42,7 +42,7 @@ import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ThreadAssemblerTest {
class ThreadDualAxisChartAssemblerTest {
val db =
"""
[
@@ -125,7 +125,7 @@ class ThreadAssemblerTest {
var counter = 0
eventArray.forEach {
TestCase.assertTrue("${it.id} failed signature check", it.hasValidSignature())
TestCase.assertTrue("${it.id} failed signature check", it.verify())
LocalCache.verifyAndConsume(it, null)
counter++
}
@@ -27,8 +27,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.actions.buildAnnotatedStringWithUrlHighlighting
import com.vitorpamplona.quartz.nip01Core.UserMetadata
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -45,15 +45,15 @@ import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
@@ -57,8 +57,8 @@ import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.ammolite.relays.NostrClient
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
@@ -110,13 +110,13 @@ class ServiceManager(
else -> HttpClientManager.setDefaultProxy(null)
}
OtsEvent.otsInstance =
OtsResolver.ots =
OpenTimestamps(
OkHttpBlockstreamExplorer(myAccount::shouldUseTorForMoneyOperations),
OkHttpCalendarBuilder(myAccount::shouldUseTorForMoneyOperations),
)
} else {
OtsEvent.otsInstance =
OtsResolver.ots =
OpenTimestamps(
OkHttpBlockstreamExplorer { false },
OkHttpCalendarBuilder { false },
File diff suppressed because it is too large Load Diff
@@ -30,13 +30,13 @@ import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
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.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
@@ -26,8 +26,8 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.note.njumpLink
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.RelayStats
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import kotlinx.coroutines.flow.MutableStateFlow
@@ -28,12 +28,16 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip19Bech32.toNAddr
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.toNEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.utils.Hex
import kotlinx.coroutines.Dispatchers
@@ -41,11 +45,23 @@ import kotlinx.coroutines.Dispatchers
class PublicChatChannel(
idHex: String,
) : Channel(idHex) {
var info = ChannelCreateEvent.ChannelData(null, null, null)
var event: ChannelCreateEvent? = null
var info = ChannelData(null, null, null, null)
override fun relays() = info.relays ?: super.relays()
fun updateChannelInfo(
creator: User,
channelInfo: ChannelCreateEvent.ChannelData,
channelInfo: ChannelCreateEvent,
updatedAt: Long,
) {
this.event = channelInfo
updateChannelInfo(creator, channelInfo.channelInfo(), updatedAt)
}
fun updateChannelInfo(
creator: User,
channelInfo: ChannelData,
updatedAt: Long,
) {
this.info = channelInfo
@@ -66,16 +82,20 @@ class PublicChatChannel(
@Stable
class LiveActivitiesChannel(
val address: ATag,
) : Channel(address.toTag()) {
val address: Address,
) : Channel(address.toValue()) {
var info: LiveActivitiesEvent? = null
override fun idNote() = address.toNAddr()
override fun idNote() = toNAddr()
override fun idDisplayNote() = idNote().toShortenHex()
fun address() = address
override fun relays() = info?.allRelayUrls() ?: super.relays()
fun relayHintUrl() = relays().firstOrNull()
fun updateChannelInfo(
creator: User,
channelInfo: LiveActivitiesEvent,
@@ -95,8 +115,16 @@ class LiveActivitiesChannel(
listOfNotNull(info?.title(), info?.summary())
.filter { it.contains(prefix, true) }
.isNotEmpty()
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrl())
fun toATag() = ATag(address, relayHintUrl())
}
data class Counter(
var number: Int = 0,
)
@Stable
abstract class Channel(
val idHex: String,
@@ -105,6 +133,7 @@ abstract class Channel(
var updatedMetadataAt: Long = 0
val notes = LargeCache<HexKey, Note>()
var lastNoteCreatedAt: Long = 0
private var relays = mapOf<RelayBriefInfoCache.RelayBriefInfo, Counter>()
open fun id() = Hex.decode(idHex)
@@ -120,6 +149,14 @@ abstract class Channel(
open fun profilePicture(): String? = creator?.info?.banner
open fun relays() =
relays.keys
.toSortedSet { o1, o2 ->
val o1Count = relays[o1]?.number ?: 0
val o2Count = relays[o2]?.number ?: 0
o2Count.compareTo(o1Count) // descending
}.map { it.url }
open fun updateChannelInfo(
creator: User,
updatedAt: Long,
@@ -130,12 +167,35 @@ abstract class Channel(
live.invalidateData()
}
fun addNote(note: Note) {
@Synchronized
fun addRelaySync(briefInfo: RelayBriefInfoCache.RelayBriefInfo) {
if (briefInfo !in relays) {
relays = relays + Pair(briefInfo, Counter(1))
}
}
fun addRelay(relay: Relay) {
val counter = relays[relay.brief]
if (counter != null) {
counter.number++
} else {
addRelaySync(relay.brief)
}
}
fun addNote(
note: Note,
relay: Relay? = null,
) {
notes.put(note.idHex, note)
if ((note.createdAt() ?: 0) > lastNoteCreatedAt) {
lastNoteCreatedAt = note.createdAt() ?: 0
}
if (relay != null) {
addRelay(relay)
}
}
fun removeNote(note: Note) {
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
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
@@ -31,49 +31,52 @@ import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.ammolite.relays.BundledInsert
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.blossom.BlossomServersEvent
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent
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.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
import com.vitorpamplona.quartz.experimental.nip95.FileStorageEvent
import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.checkSignature
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.tagValueContains
import com.vitorpamplona.quartz.nip01Core.hasValidSignature
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.addressables.mapTaggedAddress
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag
import com.vitorpamplona.quartz.nip01Core.tags.events.forEachTaggedEventId
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
import com.vitorpamplona.quartz.nip01Core.tags.events.mapTaggedEventId
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers
import com.vitorpamplona.quartz.nip01Core.verify
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.VerificationState
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.ChatroomKey
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
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip19Bech32.decodeEventIdAsHexOrNull
@@ -82,18 +85,18 @@ import com.vitorpamplona.quartz.nip19Bech32.isATag
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelHideMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMuteUserEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip34Git.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.GitPatchEvent
import com.vitorpamplona.quartz.nip34Git.GitReplyEvent
import com.vitorpamplona.quartz.nip34Git.GitRepositoryEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
@@ -113,8 +116,8 @@ import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
@@ -122,27 +125,28 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
import com.vitorpamplona.quartz.nip96FileStorage.FileServersEvent
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -272,6 +276,8 @@ object LocalCache {
fun getAddressableNoteIfExists(key: String): AddressableNote? = addressables.get(key)
fun getAddressableNoteIfExists(address: Address): AddressableNote? = getAddressableNoteIfExists(address.toValue())
fun getNoteIfExists(key: String): Note? = addressables.get(key) ?: notes.get(key)
fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId)
@@ -312,7 +318,7 @@ object LocalCache {
val noteEvent = note.event
return if (noteEvent is AddressableEvent) {
// upgrade to the latest
val newNote = checkGetOrCreateAddressableNote(noteEvent.address().toTag())
val newNote = checkGetOrCreateAddressableNote(noteEvent.aTag().toTag())
if (newNote != null && newNote.event == null) {
val author = note.author ?: getOrCreateUser(noteEvent.pubKey)
@@ -366,9 +372,10 @@ object LocalCache {
if (isValidHex(key)) {
return channels.getOrCreate(key) { PublicChatChannel(key) }
}
val aTag = ATag.parse(key, null)
if (aTag != null) {
return channels.getOrCreate(aTag.toTag()) { LiveActivitiesChannel(aTag) }
val address = Address.parse(key)
if (address != null) {
return channels.getOrCreate(address.toValue()) { LiveActivitiesChannel(address) }
}
return null
}
@@ -382,7 +389,7 @@ object LocalCache {
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? =
try {
val addr = ATag.parse(key, null) // relay doesn't matter for the index.
val addr = Address.parse(key)
if (addr != null) {
getOrCreateAddressableNote(addr)
} else {
@@ -393,17 +400,12 @@ object LocalCache {
null
}
fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote {
// checkNotInMainThread()
// we can't use naddr here because naddr might include relay info and
// the preferred relay should not be part of the index.
return addressables.getOrCreate(key.toTag()) {
fun getOrCreateAddressableNoteInternal(key: Address): AddressableNote =
addressables.getOrCreate(key.toValue()) {
AddressableNote(key)
}
}
fun getOrCreateAddressableNote(key: ATag): AddressableNote {
fun getOrCreateAddressableNote(key: Address): AddressableNote {
val note = getOrCreateAddressableNoteInternal(key)
// Loads the user outside a Syncronized block to avoid blocking
if (note.author == null) {
@@ -412,13 +414,18 @@ object LocalCache {
return note
}
fun getOrCreateNote(key: ETag): Note {
fun getOrCreateNote(key: GenericETag): Note {
val note = getOrCreateNote(key.eventId)
// Loads the user outside a Syncronized block to avoid blocking
val possibleAuthor = key.authorPubKeyHex
val possibleAuthor = key.author
if (note.author == null && possibleAuthor != null) {
note.author = checkGetOrCreateUser(possibleAuthor)
}
val relayHint = key.relay
if (!relayHint.isNullOrBlank()) {
val relay = RelayBriefInfoCache.get(RelayUrlFormatter.normalize(relayHint))
note.addRelayBrief(relay)
}
return note
}
@@ -521,7 +528,7 @@ object LocalCache {
val replyTo = computeReplyTo(event)
if (event is BaseTextNoteEvent && antiSpam.isSpam(event, relay)) {
if (event is BaseThreadedEvent && antiSpam.isSpam(event, relay)) {
return
}
@@ -676,12 +683,14 @@ object LocalCache {
is BadgeAwardEvent -> event.awardDefinition().map { getOrCreateAddressableNote(it) }
is PrivateDmEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
is RepostEvent ->
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
event.boostedEventIds().mapNotNull { checkGetOrCreateNote(it) } +
event.boostedAddresses().map { getOrCreateAddressableNote(it) }
is GenericRepostEvent ->
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
is CommunityPostApprovalEvent -> event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) }
event.boostedEventIds().mapNotNull { checkGetOrCreateNote(it) } +
event.boostedAddresses().map { getOrCreateAddressableNote(it) }
is CommunityPostApprovalEvent ->
event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } +
event.approvedAddresses().map { getOrCreateAddressableNote(it) }
is ReactionEvent ->
event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
@@ -691,7 +700,7 @@ object LocalCache {
is ChannelMessageEvent ->
event
.tagsWithoutCitations()
.filter { it != event.channel() }
.filter { it != event.channelId() }
.mapNotNull { checkGetOrCreateNote(it) }
is LiveActivitiesChatMessageEvent ->
event
@@ -731,11 +740,13 @@ object LocalCache {
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
val channel =
getOrCreateChannel(note.idHex) { LiveActivitiesChannel(note.address) }
as? LiveActivitiesChannel
val channel = getOrCreateChannel(note.idHex) { LiveActivitiesChannel(note.address) } as? LiveActivitiesChannel
val creator = event.host()?.ifBlank { null }?.let { checkGetOrCreateUser(it) } ?: author
if (relay != null) {
channel?.addRelay(relay)
}
val creator = event.host()?.let { checkGetOrCreateUser(it.pubKey) } ?: author
channel?.updateChannelInfo(creator, event, event.createdAt)
@@ -1119,7 +1130,7 @@ object LocalCache {
}
}
val addressList = event.deleteAddressTags()
val addressList = event.deleteAddressIds()
val addressSet = addressList.toSet()
addressList
@@ -1182,7 +1193,7 @@ object LocalCache {
getChannelIfExists(it.toTag())?.removeNote(deleteNote)
}
(deletedEvent as? TorrentCommentEvent)?.torrent()?.let {
(deletedEvent as? TorrentCommentEvent)?.torrentIds()?.let {
getNoteIfExists(it)?.removeReply(deleteNote)
}
@@ -1284,7 +1295,7 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey)
val communities = event.communities()
val communities = event.communityAddresses()
val eventsApproved = computeReplyTo(event)
val repliesTo = communities.map { getOrCreateAddressableNote(it) }
@@ -1353,14 +1364,17 @@ object LocalCache {
refreshObservers(note)
}
fun consume(event: ChannelCreateEvent) {
fun consume(
event: ChannelCreateEvent,
relay: Relay?,
) {
// Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
val oldChannel = getOrCreateChannel(event.id) { PublicChatChannel(it) }
val author = getOrCreateUser(event.pubKey)
val note = getOrCreateNote(event.id)
if (note.event == null) {
oldChannel.addNote(note)
oldChannel.addNote(note, relay)
note.loadEvent(event, author, emptyList())
refreshObservers(note)
@@ -1371,13 +1385,16 @@ object LocalCache {
}
if (oldChannel.creator == null || oldChannel.creator == author) {
if (oldChannel is PublicChatChannel) {
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
oldChannel.updateChannelInfo(author, event, event.createdAt)
}
}
}
fun consume(event: ChannelMetadataEvent) {
val channelId = event.channel()
fun consume(
event: ChannelMetadataEvent,
relay: Relay?,
) {
val channelId = event.channelId()
// Log.d("MT", "New PublicChatMetadata ${event.channelInfo()}")
if (channelId.isNullOrBlank()) return
@@ -1396,7 +1413,7 @@ object LocalCache {
val note = getOrCreateNote(event.id)
if (note.event == null) {
oldChannel.addNote(note)
oldChannel.addNote(note, relay)
note.loadEvent(event, author, emptyList())
refreshObservers(note)
@@ -1407,14 +1424,14 @@ object LocalCache {
event: ChannelMessageEvent,
relay: Relay?,
) {
val channelId = event.channel()
val channelId = event.channelId()
if (channelId.isNullOrBlank()) return
val channel = checkGetOrCreateChannel(channelId) ?: return
val note = getOrCreateNote(event.id)
channel.addNote(note)
channel.addNote(note, relay)
val author = getOrCreateUser(event.pubKey)
@@ -1452,12 +1469,12 @@ object LocalCache {
event: LiveActivitiesChatMessageEvent,
relay: Relay?,
) {
val activityId = event.activity() ?: return
val activityAddress = event.activityAddress() ?: return
val channel = getOrCreateChannel(activityId.toTag()) { LiveActivitiesChannel(activityId) }
val channel = getOrCreateChannel(activityAddress.toValue()) { LiveActivitiesChannel(activityAddress) }
val note = getOrCreateNote(event.id)
channel.addNote(note)
channel.addNote(note, relay)
val author = getOrCreateUser(event.pubKey)
@@ -1845,6 +1862,21 @@ object LocalCache {
)
}
/**
* Will return true if supplied note is one of events to be excluded from
* search results.
*/
private fun excludeNoteEventFromSearchResults(note: Note): Boolean =
(
note.event is GenericRepostEvent ||
note.event is RepostEvent ||
note.event is CommunityPostApprovalEvent ||
note.event is ReactionEvent ||
note.event is LnZapEvent ||
note.event is LnZapRequestEvent ||
note.event is FileHeaderEvent
)
fun findNotesStartingWith(
text: String,
forAccount: Account,
@@ -1855,19 +1887,13 @@ object LocalCache {
if (key != null) {
val note = getNoteIfExists(key)
if (note != null) {
if ((note != null) && !excludeNoteEventFromSearchResults(note)) {
return listOfNotNull(note)
}
}
return notes.filter { _, note ->
if (note.event is GenericRepostEvent ||
note.event is RepostEvent ||
note.event is CommunityPostApprovalEvent ||
note.event is ReactionEvent ||
note.event is LnZapEvent ||
note.event is LnZapRequestEvent
) {
if (excludeNoteEventFromSearchResults(note)) {
return@filter false
}
@@ -1892,13 +1918,7 @@ object LocalCache {
return@filter false
} +
addressables.filter { _, addressable ->
if (addressable.event is GenericRepostEvent ||
addressable.event is RepostEvent ||
addressable.event is CommunityPostApprovalEvent ||
addressable.event is ReactionEvent ||
addressable.event is LnZapEvent ||
addressable.event is LnZapRequestEvent
) {
if (excludeNoteEventFromSearchResults(addressable)) {
return@filter false
}
@@ -2115,7 +2135,7 @@ object LocalCache {
val noteEvent = note.event
if (noteEvent is AddressableEvent) {
noteEvent.createdAt <
(addressables.get(noteEvent.address().toTag())?.event?.createdAt ?: 0)
(addressables.get(noteEvent.aTag().toTag())?.event?.createdAt ?: 0)
} else {
false
}
@@ -2124,7 +2144,7 @@ object LocalCache {
val childrenToBeRemoved = mutableListOf<Note>()
toBeRemoved.forEach {
val newerVersion = (it.event as? AddressableEvent)?.address()?.toTag()?.let { tag -> addressables.get(tag) }
val newerVersion = (it.event as? AddressableEvent)?.aTag()?.toTag()?.let { tag -> addressables.get(tag) }
if (newerVersion != null) {
it.moveAllReferencesTo(newerVersion)
}
@@ -2308,7 +2328,7 @@ object LocalCache {
fun justVerify(event: Event): Boolean {
checkNotInMainThread()
return if (!event.hasValidSignature()) {
return if (!event.verify()) {
try {
event.checkSignature()
} catch (e: Exception) {
@@ -2391,9 +2411,9 @@ object LocalCache {
}
}
is ChannelMessageEvent -> {
draft.channel()?.let { channelId ->
draft.channelId()?.let { channelId ->
checkGetOrCreateChannel(channelId)?.let { channel ->
channel.addNote(note)
channel.addNote(note, null)
}
}
}
@@ -2460,7 +2480,7 @@ object LocalCache {
}
}
is ChannelMessageEvent -> {
draft.channel()?.let { channelId ->
draft.channelId()?.let { channelId ->
checkGetOrCreateChannel(channelId)?.let { channel ->
channel.removeNote(draftWrap)
}
@@ -2519,11 +2539,11 @@ object LocalCache {
is CalendarDateSlotEvent -> consume(event, relay)
is CalendarTimeSlotEvent -> consume(event, relay)
is CalendarRSVPEvent -> consume(event, relay)
is ChannelCreateEvent -> consume(event)
is ChannelCreateEvent -> consume(event, relay)
is ChannelListEvent -> consume(event, relay)
is ChannelHideMessageEvent -> consume(event)
is ChannelMessageEvent -> consume(event, relay)
is ChannelMetadataEvent -> consume(event)
is ChannelMetadataEvent -> consume(event, relay)
is ChannelMuteUserEvent -> consume(event)
is ChatMessageEncryptedFileHeaderEvent -> consume(event, relay)
is ChatMessageEvent -> consume(event, relay)
@@ -37,35 +37,41 @@ import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.ammolite.relays.filters.EOSETime
import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue
import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
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
import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.anyHashTag
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.toNAddr
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.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.PayInvoiceSuccessResponse
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.containsAny
@@ -80,11 +86,11 @@ import kotlin.coroutines.resume
@Stable
class AddressableNote(
val address: ATag,
) : Note(address.toTag()) {
override fun idNote() = address.toNAddr(relayHintUrl())
val address: Address,
) : Note(address.toValue()) {
override fun idNote() = toNAddr()
override fun toNEvent() = address.toNAddr(relayHintUrl())
override fun toNEvent() = toNAddr()
override fun idDisplayNote() = idNote().toShortenHex()
@@ -103,11 +109,15 @@ class AddressableNote(
override fun wasOrShouldBeDeletedBy(
deletionEvents: Set<HexKey>,
deletionAddressables: Set<ATag>,
deletionAddressables: Set<Address>,
): Boolean {
val thisEvent = event
return deletionAddressables.contains(address) || (thisEvent != null && deletionEvents.contains(thisEvent.id))
}
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrl())
fun toATag() = ATag(address, relayHintUrl())
}
@Stable
@@ -195,16 +205,16 @@ open class Note(
event is LiveActivitiesChatMessageEvent ||
event is LiveActivitiesEvent
) {
(event as? ChannelMessageEvent)?.channel()
?: (event as? ChannelMetadataEvent)?.channel()
(event as? ChannelMessageEvent)?.channelId()
?: (event as? ChannelMetadataEvent)?.channelId()
?: (event as? ChannelCreateEvent)?.id
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
?: (event as? LiveActivitiesEvent)?.address()?.toTag()
?: (event as? LiveActivitiesEvent)?.aTag()?.toTag()
} else {
null
}
open fun address(): ATag? = null
open fun address(): Address? = null
open fun createdAt() = event?.createdAt
@@ -442,6 +452,13 @@ open class Note(
}
}
fun addRelayBrief(brief: RelayBriefInfoCache.RelayBriefInfo) {
if (brief !in relays) {
addRelaySync(brief)
flowSet?.relays?.invalidateData()
}
}
private suspend fun isPaidByCalculation(
account: Account,
zapEvents: List<Pair<Note, Note?>>,
@@ -702,7 +719,7 @@ open class Note(
fun hasPledgeBy(user: User): Boolean =
replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.filter { it.event?.hasAdditionalReward() ?: false }
.any {
val pledgeValue =
try {
@@ -716,18 +733,7 @@ open class Note(
pledgeValue != null && it.author == user
}
fun pledgedAmountByOthers(): BigDecimal =
replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.mapNotNull {
try {
BigDecimal(it.event?.content)
} catch (e: Exception) {
if (e is CancellationException) throw e
null
// do nothing if it can't convert to bigdecimal
}
}.sumOf { it }
fun pledgedAmountByOthers(): BigDecimal = replies.sumOf { it.event?.addedRewardValue() ?: BigDecimal.ZERO }
fun hasAnyReports(): Boolean {
val dayAgo = TimeUtils.oneDayAgo()
@@ -839,7 +845,7 @@ open class Note(
}
if (accountChoices.hiddenWordsCase.isNotEmpty()) {
if (thisEvent is BaseTextNoteEvent && thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) {
if (thisEvent is BaseThreadedEvent && thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) {
return true
}
@@ -912,11 +918,41 @@ open class Note(
open fun wasOrShouldBeDeletedBy(
deletionEvents: Set<HexKey>,
deletionAddressables: Set<ATag>,
deletionAddressables: Set<Address>,
): Boolean {
val thisEvent = event
return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.address()))
}
fun toETag(): ETag {
val noteEvent = event
return if (noteEvent != null) {
ETag(noteEvent.id, relayHintUrl(), noteEvent.pubKey)
} else {
ETag(idHex, relayHintUrl(), author?.pubkeyHex)
}
}
fun toEId(): EventReference {
val noteEvent = event
return if (noteEvent != null) {
// uses the confirmed event id if available
EventReference(noteEvent.id, noteEvent.pubKey, relayHintUrl())
} else {
EventReference(idHex, author?.pubkeyHex, relayHintUrl())
}
}
fun <T : Event> toEventHint() = (event as? T)?.let { EventHintBundle(it, relayHintUrl(), author?.bestRelayHint()) }
fun toMarkedETag(marker: MarkedETag.MARKER): MarkedETag {
val noteEvent = event
return if (noteEvent != null) {
MarkedETag(noteEvent.id, relayHintUrl(), marker, noteEvent.pubKey)
} else {
MarkedETag(idHex, relayHintUrl(), marker, author?.pubkeyHex)
}
}
}
@Stable
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey
class ParticipantListBuilder {
private fun addFollowsThatDirectlyParticipateOnToSet(
@@ -35,6 +35,7 @@ data class Settings(
val dontShowPushNotificationSelector: Boolean = false,
val dontAskForNotificationPermissions: Boolean = false,
val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC,
)
enum class ThemeType(
@@ -75,6 +76,14 @@ enum class FeatureSetType(
PERFORMANCE(2, R.string.ui_feature_set_type_performance),
}
enum class ProfileGalleryType(
val screenCode: Int,
val resourceId: Int,
) {
CLASSIC(0, R.string.gallery_type_classic),
MODERN(1, R.string.gallery_type_modern),
}
fun parseConnectivityType(code: Boolean?): ConnectivityType =
when (code) {
ConnectivityType.ALWAYS.prefCode -> ConnectivityType.ALWAYS
@@ -105,6 +114,15 @@ fun parseFeatureSetType(screenCode: Int): FeatureSetType =
}
}
fun parseGalleryType(screenCode: Int): ProfileGalleryType =
when (screenCode) {
ProfileGalleryType.CLASSIC.screenCode -> ProfileGalleryType.CLASSIC
ProfileGalleryType.MODERN.screenCode -> ProfileGalleryType.MODERN
else -> {
ProfileGalleryType.CLASSIC
}
}
enum class BooleanType(
val prefCode: Boolean?,
val screenCode: Int,
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import kotlinx.collections.immutable.ImmutableSet
@@ -132,7 +132,7 @@ class ThreadAssembler {
}
class OnlyLatestVersionSet : MutableSet<Note> {
val map = hashMapOf<ATag, Long>()
val map = hashMapOf<Address, Long>()
val set = hashSetOf<Note>()
override fun add(element: Note): Boolean {
@@ -149,7 +149,7 @@ class OnlyLatestVersionSet : MutableSet<Note> {
}
private fun innerAdd(
address: ATag,
address: Address,
element: Note,
loadedCreatedAt: Long,
): Boolean {
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import java.lang.Long.min
@@ -32,15 +32,16 @@ import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.filters.EOSETime
import com.vitorpamplona.quartz.lightning.Lud06
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.UserMetadata
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
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.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
@@ -88,14 +89,15 @@ class User(
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
fun toNProfile(): String {
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)?.writeRelays()
fun authorRelayList() = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)
return NProfile.create(
pubkeyHex,
relayList?.take(3) ?: listOfNotNull(latestMetadataRelay),
)
}
fun toNProfile() = NProfile.create(pubkeyHex, relayHints())
fun relayHints() = authorRelayList()?.writeRelays()?.take(3) ?: listOfNotNull(latestMetadataRelay)
fun bestRelayHint() = authorRelayList()?.writeRelays()?.firstOrNull() ?: latestMetadataRelay
fun toPTag() = PTag(pubkeyHex, bestRelayHint())
fun toNostrUri() = "nostr:${toNProfile()}"
@@ -240,16 +242,13 @@ class User(
}.flatten()
@Synchronized
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom {
checkNotInMainThread()
return privateChatrooms[key]
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))
@@ -44,7 +44,7 @@ abstract class AmethystNostrDataSource(
val note = LocalCache.getNoteIfExists(eventId)
val noteEvent = note?.event
if (noteEvent is AddressableEvent) {
LocalCache.getAddressableNoteIfExists(noteEvent.address().toTag())?.addRelay(relay)
LocalCache.getAddressableNoteIfExists(noteEvent.aTag().toTag())?.addRelay(relay)
} else {
note?.addRelay(relay)
}
@@ -34,8 +34,8 @@ import coil3.key.Keyer
import coil3.request.ImageRequest
import coil3.request.Options
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.base64contentPattern
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.sha256.sha256
import java.util.Base64
@Stable
@@ -83,7 +83,7 @@ class Base64Fetcher(
options: Options,
): String? =
if (data.scheme == "data") {
CryptoUtils.sha256(data.toString().toByteArray()).toHexKey()
sha256(data.toString().toByteArray()).toHexKey()
} else {
null
}
@@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.ExperimentalSerializationApi
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2
@@ -125,5 +126,9 @@ fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists<String>): String {
}
}
if (EmojiCoder.isCoded(this)) {
return EmojiCoder.cropToFirstMessage(this)
}
return firstFullChar()
}
@@ -34,6 +34,7 @@ import com.fonfon.kgeohash.GeoHash
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_DISTANCE
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_TIME
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -125,7 +126,7 @@ class LocationState(
(context)
.get(MIN_TIME, MIN_DISTANCE)
.map {
LocationResult.Success(it.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits)) as LocationResult
LocationResult.Success(it.toGeoHash(GeohashPrecision.KM_5_X_5.digits)) as LocationResult
}.onEach {
latestLocation = it
}.catch { e ->
@@ -35,21 +35,21 @@ import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip34Git.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.GitPatchEvent
import com.vitorpamplona.quartz.nip34Git.GitReplyEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
@@ -64,12 +64,12 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip96FileStorage.FileServersEvent
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import com.vitorpamplona.quartz.utils.TimeUtils
// TODO: Migrate this to a property of AccountVi
@@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
var account: Account? = null
@@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.ChatroomKey
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") {
lateinit var account: Account
@@ -26,10 +26,10 @@ import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
lateinit var account: Account
@@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed") {
private var communityToWatch: AddressableNote? = null
@@ -38,7 +38,7 @@ object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed")
val authors =
community
.moderators()
.map { it.key }
.map { it.pubKey }
.plus(listOfNotNull(myCommunityToWatch.author?.pubkeyHex))
if (authors.isEmpty()) return null
@@ -50,7 +50,7 @@ object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed")
authors = authors,
tags =
mapOf(
"a" to listOf(myCommunityToWatch.address.toTag()),
"a" to listOf(myCommunityToWatch.address.toValue()),
),
kinds = listOf(CommunityPostApprovalEvent.KIND),
limit = 500,
@@ -27,14 +27,14 @@ import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
@@ -23,15 +23,15 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
@@ -27,22 +27,22 @@ import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
import kotlinx.coroutines.Dispatchers
@@ -24,19 +24,19 @@ import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
@@ -44,19 +44,18 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip19Bech32.parse
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
import com.vitorpamplona.quartz.utils.Hex
@@ -81,7 +80,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
}
when (val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity) {
is NSec -> KeyPair(privKey = parsed.hex.bechToBytes()).pubKey.toHexKey()
is NSec -> Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey()
is NPub -> parsed.hex
is NProfile -> parsed.hex
is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> parsed.hex
@@ -27,8 +27,8 @@ import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed") {
private var channelsToWatch = setOf<Channel>()
@@ -35,12 +35,12 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip34Git.GitReplyEvent
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
@@ -81,7 +81,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
CommunityPostApprovalEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
),
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
tags = mapOf("a" to it.mapNotNull { it.address()?.toValue() }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 1000,
@@ -95,7 +95,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
listOf(
DeletionEvent.KIND,
),
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
tags = mapOf("a" to it.mapNotNull { it.address()?.toValue() }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 10,
@@ -26,8 +26,8 @@ import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.EOSETime
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
@@ -24,12 +24,12 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
@@ -49,7 +49,7 @@ import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
var user: User? = null
@@ -27,7 +27,7 @@ import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
@@ -25,7 +25,7 @@ import android.util.LruCache
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.utils.RandomInstance
import okhttp3.EventListener
import okhttp3.Protocol
import okhttp3.Request
@@ -70,7 +70,7 @@ object OnlineChecker {
.url(url.replace("wss+livekit://", "wss://"))
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Key", CryptoUtils.random(16).toByteString().base64())
.header("Sec-WebSocket-Key", RandomInstance.bytes(16).toByteString().base64())
.header("Sec-WebSocket-Version", "13")
.header("Sec-WebSocket-Extensions", "permessage-deflate")
.build()
@@ -32,14 +32,14 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.splits.BaseZapSplitSetup
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.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -77,7 +77,7 @@ class ZapPaymentHandler(
if (!zapSplitSetup.isNullOrEmpty()) {
zapSplitSetup
} else if (noteEvent is LiveActivitiesEvent && noteEvent.hasHost()) {
noteEvent.hosts().map { ZapSplitSetup(it.pubKeyHex, it.relay, weight = 1.0) }
noteEvent.hosts().map { ZapSplitSetup(it.pubKey, it.relayHint, weight = 1.0) }
} else if (noteEvent is AppDefinitionEvent) {
val appLud16 = noteEvent.appMetaData()?.lnAddress()
if (appLud16 != null) {
@@ -37,18 +37,18 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
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.nip55AndroidSigner.NostrSignerExternal
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import java.math.BigDecimal
import kotlin.coroutines.cancellation.CancellationException
@@ -137,7 +137,7 @@ class EventNotificationConsumer(
Log.d(TAG, "New Notification Arrived")
if (!LocalCache.justVerify(event)) return
val users = event.taggedUsers().map { LocalCache.getOrCreateUser(it) }
val users = event.taggedUserIds().map { LocalCache.getOrCreateUser(it) }
val npubs = users.map { it.pubkeyNpub() }.toSet()
// PushNotification Wraps don't include a receiver.
@@ -20,51 +20,85 @@
*/
package com.vitorpamplona.amethyst.service.okhttp
import android.util.Log
import com.vitorpamplona.quartz.nip17Dm.AESGCM
import com.vitorpamplona.quartz.nip17Dm.NostrCipher
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
class EncryptedBlobInterceptor(
val cache: EncryptionKeyCache,
) : Interceptor {
fun Response.decrypt(cipher: NostrCipher): Response {
private fun Response.decryptOrNullWithErrorCorrection(info: DecryptInformation): Response? {
val body = peekBody(Long.MAX_VALUE)
val decryptedBytes = cipher.decrypt(body.bytes())
val newBody = decryptedBytes.toResponseBody(body.contentType())
return newBuilder().body(newBody).build()
}
fun Response.decryptOrNull(cipher: NostrCipher): Response? =
try {
decrypt(cipher)
} catch (e: Exception) {
Log.w("EncryptedBlobInterceptor", "Failed to decrypt", e)
null
// Only tries to decrypt if the content-type is a byte array
if (body.contentType().toString() != "application/octet-stream") {
return null
}
private fun Response.decryptOrNullWithErrorCorrection(cipher: NostrCipher): Response? {
return decryptOrNull(cipher) ?: return if (cipher is AESGCM) {
decryptOrNull(cipher.copyUsingUTF8Nonce())
} else {
null
val bytes = body.bytes()
// Tries the correct way first
// if it fails, tries to decrypt as UTF8 nonce, which was how
// 0xChat started encrypting
val decrypted =
info.cipher.decryptOrNull(bytes) ?: if (info.cipher is AESGCM) {
info.cipher.copyUsingUTF8Nonce().decryptOrNull(bytes)
} else {
null
}
if (decrypted == null) {
return null
}
return newBuilder()
.apply {
body(
decrypted.toResponseBody(
info.mimeType?.toMediaTypeOrNull() ?: body.contentType(),
),
)
// removes hints that would make the app requrest partial byte arrays
// in videos, which are impossible to decrypt.
removeHeader("accept-ranges")
// Fixes the size of the body array
header("content-length", decrypted.size.toString())
// Trusts the mimetype from the event is better than the mimetype from the server
info.mimeType?.let { header("content-type", it) }
}.build()
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val encryptionKeys = cache.get(request.url.toString())
val cipher = cache.get(request.url.toString()) ?: return response
// We cannot use Range requests (partial byte arrays)
// in encrypted payloads because we won't be able to
// decrypt partial byte arrays.
val newRequest =
if (encryptionKeys != null) {
request
.newBuilder()
.removeHeader("Range")
.build()
} else {
request
}
val response = chain.proceed(newRequest)
if (encryptionKeys == null) {
return response
}
if (response.isSuccessful) {
return response.decryptOrNullWithErrorCorrection(cipher) ?: response
return response.decryptOrNullWithErrorCorrection(encryptionKeys) ?: response
} else {
// Log redirections to be able to use the cipher.
response.header("Location")?.let {
cache.add(it, cipher)
cache.add(it, encryptionKeys)
}
}
return response
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.okhttp
import android.util.LruCache
import com.vitorpamplona.quartz.nip17Dm.NostrCipher
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
/**
* Neigther ExoPlayer, nor Coil support passing key and nonce to the Interceptor via
@@ -30,16 +30,27 @@ import com.vitorpamplona.quartz.nip17Dm.NostrCipher
* This class serves as a key cache to decrypt the body of HTTP calls that need it.
*/
class EncryptionKeyCache {
val cache = LruCache<String, NostrCipher>(100)
val cache = LruCache<String, DecryptInformation>(100)
fun add(
url: String?,
decryptInformation: DecryptInformation,
) {
if (cache.get(url) == null) {
cache.put(url, decryptInformation)
}
}
fun add(
url: String?,
cipher: NostrCipher,
) {
if (cache.get(url) == null) {
cache.put(url, cipher)
}
}
expectedMimeType: String?,
) = add(url, DecryptInformation(cipher, expectedMimeType))
fun get(url: String): NostrCipher? = cache.get(url)
fun get(url: String): DecryptInformation? = cache.get(url)
}
class DecryptInformation(
val cipher: NostrCipher,
val mimeType: String?,
)
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.okhttp
import android.util.Log
import com.vitorpamplona.quartz.nip17Dm.NostrCipher
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
import okhttp3.OkHttpClient
import java.net.InetSocketAddress
import java.net.Proxy
@@ -124,5 +124,6 @@ object HttpClientManager {
fun addCipherToCache(
url: String,
cipher: NostrCipher,
) = cache.add(url, cipher)
expectedMimeType: String?,
) = cache.add(url, cipher, expectedMimeType)
}
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.service.okhttp
import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebSocket
import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebSocketListener
import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebsocketBuilder
import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebsocketBuilderFactory
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory
import okhttp3.Request
import okhttp3.Response
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service.uploads
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip17Dm.NostrCipher
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
import com.vitorpamplona.quartz.utils.sha256.sha256
import java.io.File
class EncryptFilesResult(
@@ -48,9 +48,9 @@ class EncryptFiles {
resolver.openInputStream(inputFile)!!.use { inputStream ->
val bytes = inputStream.readBytes()
val originalHash = CryptoUtils.sha256(bytes).toHexKey()
val originalHash = sha256(bytes).toHexKey()
val encrypted = cipher.encrypt(bytes)
val encryptedHash = CryptoUtils.sha256(encrypted).toHexKey()
val encryptedHash = sha256(encrypted).toHexKey()
encryptedFile.outputStream().use { outputStream ->
outputStream.write(encrypted)
@@ -30,9 +30,9 @@ import android.os.Build
import android.util.Log
import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
import com.vitorpamplona.amethyst.service.Blurhash
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip94FileMetadata.Dimension
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.utils.sha256.sha256
import kotlinx.coroutines.CancellationException
import java.io.IOException
@@ -40,7 +40,7 @@ class FileHeader(
val mimeType: String?,
val hash: String,
val size: Int,
val dim: Dimension?,
val dim: DimensionTag?,
val blurHash: Blurhash?,
) {
class UnableToDownload(
@@ -51,7 +51,7 @@ class FileHeader(
suspend fun prepare(
fileUrl: String,
mimeType: String?,
dimPrecomputed: Dimension?,
dimPrecomputed: DimensionTag?,
forceProxy: Boolean,
): Result<FileHeader> =
try {
@@ -71,10 +71,10 @@ class FileHeader(
fun prepare(
data: ByteArray,
mimeType: String?,
dimPrecomputed: Dimension?,
dimPrecomputed: DimensionTag?,
): Result<FileHeader> =
try {
val hash = CryptoUtils.sha256(data).toHexKey()
val hash = sha256(data).toHexKey()
val size = data.size
val (blurHash, dim) =
@@ -82,7 +82,7 @@ class FileHeader(
val opt = BitmapFactory.Options()
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt)
Pair(Blurhash(mBitmap.toBlurhash()), Dimension(mBitmap.width, mBitmap.height))
Pair(Blurhash(mBitmap.toBlurhash()), DimensionTag(mBitmap.width, mBitmap.height))
} else if (mimeType?.startsWith("video/") == true) {
val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(ByteArrayMediaDataSource(data))
@@ -135,12 +135,12 @@ fun MediaMetadataRetriever.getThumbnail(): Bitmap? {
}
}
fun MediaMetadataRetriever.prepareDimFromVideo(): Dimension? {
fun MediaMetadataRetriever.prepareDimFromVideo(): DimensionTag? {
val width = prepareVideoWidth() ?: return null
val height = prepareVideoHeight() ?: return null
return if (width > 0 && height > 0) {
Dimension(width, height)
DimensionTag(width, height)
} else {
null
}
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.service.uploads
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip94FileMetadata.Dimension
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
data class MediaUploadResult(
// A publicly accessible URL to the BUD-01 GET /<sha256> endpoint (optionally with a file extension)
@@ -35,7 +35,7 @@ data class MediaUploadResult(
// upload time
val uploaded: Long? = null,
// dimensions
val dimension: Dimension? = null,
val dimension: DimensionTag? = null,
// magnet link
val magnet: String? = null,
// info hash
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Account
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.NostrCipher
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll
@@ -49,7 +49,7 @@ class MultiOrchestrator(
suspend fun upload(
scope: CoroutineScope,
alt: String?,
sensitiveContent: Boolean,
contentWarningReason: String?,
mediaQuality: CompressorQuality,
server: ServerName,
account: Account,
@@ -62,7 +62,7 @@ class MultiOrchestrator(
item.media.uri,
item.media.mimeType,
alt,
sensitiveContent,
contentWarningReason,
mediaQuality,
server,
account,
@@ -79,7 +79,7 @@ class MultiOrchestrator(
suspend fun uploadEncrypted(
scope: CoroutineScope,
alt: String?,
sensitiveContent: Boolean,
contentWarningReason: String?,
mediaQuality: CompressorQuality,
cipher: NostrCipher,
server: ServerName,
@@ -93,7 +93,7 @@ class MultiOrchestrator(
item.media.uri,
item.media.mimeType,
alt,
sensitiveContent,
contentWarningReason,
mediaQuality,
cipher,
server,
@@ -126,5 +126,5 @@ class MultiOrchestrator(
fun size() = list.size
fun get(index: Int) = list.get(index)
fun get(index: Int) = list[index]
}
@@ -29,7 +29,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.nip17Dm.NostrCipher
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlin.coroutines.cancellation.CancellationException
@@ -132,7 +132,7 @@ class UploadOrchestrator {
contentType: String?,
size: Long?,
alt: String?,
sensitiveContent: Boolean,
contentWarningReason: String?,
serverBaseUrl: String,
contentTypeForResult: String?,
originalHash: String?,
@@ -147,7 +147,7 @@ class UploadOrchestrator {
contentType = contentType,
size = size,
alt = alt,
sensitiveContent = if (sensitiveContent) "" else null,
sensitiveContent = contentWarningReason,
serverBaseUrl = serverBaseUrl,
forceProxy = account::shouldUseTorForNIP96,
onProgress = { percent: Float ->
@@ -175,7 +175,7 @@ class UploadOrchestrator {
contentType: String?,
size: Long?,
alt: String?,
sensitiveContent: Boolean,
contentWarningReason: String?,
serverBaseUrl: String,
contentTypeForResult: String?,
originalHash: String?,
@@ -191,7 +191,7 @@ class UploadOrchestrator {
contentType = contentType,
size = size,
alt = alt,
sensitiveContent = if (sensitiveContent) "" else null,
sensitiveContent = contentWarningReason,
serverBaseUrl = serverBaseUrl,
forceProxy = account::shouldUseTorForNIP96,
httpAuth = account::createBlossomUploadAuth,
@@ -292,7 +292,7 @@ class UploadOrchestrator {
uri: Uri,
mimeType: String?,
alt: String?,
sensitiveContent: Boolean,
contentWarningReason: String?,
compressionQuality: CompressorQuality,
server: ServerName,
account: Account,
@@ -302,8 +302,8 @@ class UploadOrchestrator {
return when (server.type) {
ServerType.NIP95 -> uploadNIP95(compressed.uri, compressed.contentType, null, null, context)
ServerType.NIP96 -> uploadNIP96(compressed.uri, compressed.contentType, compressed.size, alt, sensitiveContent, server.baseUrl, null, null, account, context)
ServerType.Blossom -> uploadBlossom(compressed.uri, compressed.contentType, compressed.size, alt, sensitiveContent, server.baseUrl, null, null, account, context)
ServerType.NIP96 -> uploadNIP96(compressed.uri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, context)
ServerType.Blossom -> uploadBlossom(compressed.uri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, context)
}
}
@@ -311,7 +311,7 @@ class UploadOrchestrator {
uri: Uri,
mimeType: String?,
alt: String?,
sensitiveContent: Boolean,
contentWarningReason: String?,
compressionQuality: CompressorQuality,
encrypt: NostrCipher,
server: ServerName,
@@ -323,8 +323,8 @@ class UploadOrchestrator {
return when (server.type) {
ServerType.NIP95 -> uploadNIP95(encrypted.uri, encrypted.contentType, compressed.contentType, encrypted.originalHash, context)
ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, sensitiveContent, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context)
ServerType.Blossom -> uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, sensitiveContent, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context)
ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context)
ServerType.Blossom -> uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context)
}
}
}
@@ -35,10 +35,10 @@ import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.sha256.sha256
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody
@@ -88,7 +88,7 @@ class BlossomUploader {
checkNotNull(payload) { "Can't open the image input stream" }
val hash = CryptoUtils.sha256(payload).toHexKey()
val hash = sha256(payload).toHexKey()
val imageInputStream = contentResolver.openInputStream(uri)
@@ -34,13 +34,12 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip36SensitiveContent.CONTENT_WARNING
import com.vitorpamplona.quartz.nip94FileMetadata.Dimension
import com.vitorpamplona.quartz.nip96FileStorage.AuthToken
import com.vitorpamplona.quartz.nip96FileStorage.Nip96Result
import com.vitorpamplona.quartz.nip96FileStorage.PartialEvent
import com.vitorpamplona.quartz.nip96FileStorage.ResultParser
import com.vitorpamplona.quartz.nip96FileStorage.ServerInfo
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nip96FileStorage.actions.DeleteResult
import com.vitorpamplona.quartz.nip96FileStorage.actions.PartialEvent
import com.vitorpamplona.quartz.nip96FileStorage.actions.UploadResult
import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
import kotlinx.coroutines.delay
import okhttp3.MediaType.Companion.toMediaType
@@ -153,7 +152,7 @@ class Nip96Uploader {
.addFormDataPart("size", length.toString())
.also { body ->
alt?.ifBlank { null }?.let { body.addFormDataPart("alt", it) }
sensitiveContent?.let { body.addFormDataPart(CONTENT_WARNING, it) }
sensitiveContent?.let { body.addFormDataPart(ContentWarningTag.TAG_NAME, it) }
contentType?.let { body.addFormDataPart("content_type", it) }
}.addFormDataPart(
"file",
@@ -169,7 +168,7 @@ class Nip96Uploader {
},
).build()
httpAuth(server.apiUrl, "POST", null)?.let { requestBuilder.addHeader("Authorization", AuthToken().encodeAuth(it)) }
httpAuth(server.apiUrl, "POST", null)?.let { requestBuilder.addHeader("Authorization", it.toAuthToken()) }
requestBuilder
.addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
@@ -181,7 +180,7 @@ class Nip96Uploader {
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
response.body.use { body ->
val result = ResultParser().parseResults(body.string())
val result = UploadResult.parse(body.string())
if (!result.processingUrl.isNullOrBlank()) {
return waitProcessing(result, server, forceProxy, onProgress)
} else if (result.status == "success") {
@@ -244,7 +243,7 @@ class Nip96Uploader {
?.firstOrNull { it.size > 1 && it[0] == "dim" }
?.get(1)
?.ifBlank { null }
?.let { Dimension.parse(it) }
?.let { DimensionTag.parse(it) }
val magnet =
nip96.tags
?.firstOrNull { it.size > 1 && it[0] == "magnet" }
@@ -265,7 +264,7 @@ class Nip96Uploader {
contentType: String?,
server: ServerInfo,
forceProxy: (String) -> Boolean,
httpAuth: (String, String, ByteArray?) -> HTTPAuthorizationEvent,
httpAuth: (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
context: Context,
): Boolean {
val extension =
@@ -275,7 +274,7 @@ class Nip96Uploader {
val requestBuilder = Request.Builder()
httpAuth(server.apiUrl, "DELETE", null)?.let { requestBuilder.addHeader("Authorization", AuthToken().encodeAuth(it)) }
httpAuth(server.apiUrl, "DELETE", null)?.let { requestBuilder.addHeader("Authorization", it.toAuthToken()) }
val request =
requestBuilder
@@ -287,7 +286,7 @@ class Nip96Uploader {
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
response.body.use { body ->
val result = ResultParser().parseDeleteResults(body.string())
val result = DeleteResult.parse(body.string())
return result.status == "success"
}
} else {
@@ -302,7 +301,7 @@ class Nip96Uploader {
}
private suspend fun waitProcessing(
result: Nip96Result,
result: UploadResult,
server: ServerInfo,
forceProxy: (String) -> Boolean,
onProgress: (percentage: Float) -> Unit,
@@ -324,7 +323,7 @@ class Nip96Uploader {
val client = HttpClientManager.getHttpClient(forceProxy(procUrl))
client.newCall(request).execute().use {
if (it.isSuccessful) {
it.body.use { currentResult = ResultParser().parseResults(it.string()) }
it.body.use { currentResult = UploadResult.parse(it.string()) }
}
}
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.uploads.nip96
import android.util.Log
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.quartz.nip96FileStorage.ServerInfo
import com.vitorpamplona.quartz.nip96FileStorage.ServerInfoParser
import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo
import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfoParser
import kotlinx.coroutines.CancellationException
import okhttp3.Request
@@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
@@ -55,12 +55,12 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.Note
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -46,10 +46,19 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.quartz.experimental.nip95.FileStorageEvent
import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
import com.vitorpamplona.quartz.nip94FileMetadata.alt
import com.vitorpamplona.quartz.nip94FileMetadata.blurhash
import com.vitorpamplona.quartz.nip94FileMetadata.dims
import com.vitorpamplona.quartz.nip94FileMetadata.hash
import com.vitorpamplona.quartz.nip94FileMetadata.magnet
import com.vitorpamplona.quartz.nip94FileMetadata.mimeType
import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
import com.vitorpamplona.quartz.nip94FileMetadata.size
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -165,7 +174,7 @@ open class EditPostViewModel : ViewModel() {
myMultiOrchestrator.upload(
viewModelScope,
alt,
sensitiveContent,
if (sensitiveContent) "" else null,
MediaCompressor.intToCompressorQuality(mediaQuality),
server,
myAccount,
@@ -175,7 +184,12 @@ 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, sensitiveContent) { nip95 ->
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) }
@@ -81,7 +81,7 @@ import com.vitorpamplona.amethyst.ui.note.SearchIcon
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelName
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.ChannelName
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -58,17 +58,18 @@ object MediaSaverToDisk {
if (videoUri != null) {
if (!videoUri.startsWith("file")) {
downloadAndSave(
url = videoUri,
mimeType = mimeType,
context = localContext,
forceProxy = forceProxy,
url = videoUri,
onSuccess = onSuccess,
onError = onError,
)
} else {
save(
context = localContext,
localFile = videoUri.toUri().toFile(),
mimeType = mimeType,
context = localContext,
onSuccess = onSuccess,
onError = onError,
)
@@ -83,6 +84,7 @@ object MediaSaverToDisk {
*/
fun downloadAndSave(
url: String,
mimeType: String?,
forceProxy: Boolean,
context: Context,
onSuccess: () -> Any?,
@@ -121,9 +123,16 @@ object MediaSaverToDisk {
val contentType = response.header("Content-Type")
checkNotNull(contentType) { "Can't find out the content type" }
val realType =
if (mimeType != null && contentType == "application/octet-stream") {
mimeType
} else {
contentType
}
saveContentQ(
displayName = File(url).nameWithoutExtension,
contentType = contentType,
contentType = realType,
contentSource = response.body.source(),
contentResolver = context.contentResolver,
)
@@ -26,6 +26,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -53,19 +57,44 @@ class NewChannelViewModel : ViewModel() {
fun create() {
viewModelScope.launch(Dispatchers.IO) {
account?.let { account ->
if (originalChannel == null) {
account.sendCreateNewChannel(
channelName.value.text,
channelDescription.value.text,
channelPicture.value.text,
)
val channel = originalChannel
if (channel == null) {
val template =
ChannelCreateEvent.build(
channelName.value.text,
channelDescription.value.text,
channelPicture.value.text,
null,
)
account.sendCreateNewChannel(template)
} else {
account.sendChangeChannel(
channelName.value.text,
channelDescription.value.text,
channelPicture.value.text,
originalChannel!!,
)
val event = channel.event
val template =
if (event != null) {
val hint = EventHintBundle<ChannelCreateEvent>(event, channel.relays().firstOrNull())
ChannelMetadataEvent.build(
channelName.value.text,
channelDescription.value.text,
channelPicture.value.text,
null,
hint,
)
} else {
val eTag = ETag(channel.idHex, channel.relays().firstOrNull())
ChannelMetadataEvent.build(
channelName.value.text,
channelDescription.value.text,
channelPicture.value.text,
null,
eTag,
)
}
account.sendChangeChannel(template)
}
}
@@ -99,7 +99,7 @@ open class NewMediaModel : ViewModel() {
myMultiOrchestrator.upload(
viewModelScope,
caption,
sensitiveContent,
if (sensitiveContent) "" else null,
MediaCompressor.intToCompressorQuality(mediaQualitySlider),
serverToUse,
myAccount,
@@ -140,7 +140,7 @@ open class NewMediaModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
withTimeoutOrNull(30000) {
suspendCancellableCoroutine { continuation ->
account?.createNip95(it.bytes, headerInfo = it.fileHeader, caption, sensitiveContent) { nip95 ->
account?.createNip95(it.bytes, headerInfo = it.fileHeader, caption, if (sensitiveContent) "" else null) { nip95 ->
account?.consumeAndSendNip95(nip95.first, nip95.second, relayList)
continuation.resume(true)
}
@@ -160,7 +160,7 @@ open class NewMediaModel : ViewModel() {
it.magnet,
it.fileHeader,
caption,
sensitiveContent,
if (sensitiveContent) "" else null,
it.uploadedHash,
relayList,
) {
@@ -180,7 +180,7 @@ open class NewMediaModel : ViewModel() {
account?.sendAllAsOnePictureEvent(
imageUrls,
caption,
sensitiveContent,
if (sensitiveContent) "" else null,
relayList,
) {
continuation.resume(true)
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
@@ -180,7 +180,7 @@ class NewMessageTagger(
val restOfWord = key.substring(63)
// Converts to npub
val pubkey =
Nip19Parser.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
Nip19Parser.uriToRoute(Nip01.pubKeyCreate(keyB32.bechToBytes()).toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
} else if (key.startsWith("npub1", true)) {
@@ -52,7 +52,7 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) {
pollViewModel.isValidClosedAt.value = true
if (text.isNotEmpty()) {
try {
val int = text.toInt()
val int = text.toLong()
if (int < 0) {
pollViewModel.isValidClosedAt.value = false
} else {
File diff suppressed because it is too large Load Diff
@@ -142,7 +142,7 @@ fun NewUserMetadataScreen(
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState()),
) {
OutlinedTextField(
label = { Text(text = stringRes(R.string.display_name)) },
label = { Text(text = stringRes(R.string.profile_name)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.displayName.value,
onValueChange = { postViewModel.displayName.value = it },
@@ -31,7 +31,7 @@ import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDecoration
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
import kotlin.coroutines.cancellation.CancellationException
@@ -41,4 +41,5 @@ val DEFAULT_MEDIA_SERVERS: List<ServerName> =
ServerName("Satellite (Paid)", "https://cdn.satellite.earth", ServerType.Blossom),
ServerName("NostrCheck.me (Blossom)", "https://cdn.nostrcheck.me", ServerType.Blossom),
ServerName("Nostr.Download", "https://nostr.download", ServerType.Blossom),
ServerName("NostrMedia (Paid)", "https://nostrmedia.com", ServerType.Blossom),
)
@@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.nip01Core.relays.RelayStat
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.nip01Core.relays.RelayStat
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -61,7 +61,7 @@ import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.quartz.nip01Core.relays.RelayStat
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@Composable
fun AllRelayListView(
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.actions.relays
import androidx.compose.runtime.Immutable
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.relays.RelayStat
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@Immutable
data class BasicRelaySetupInfo(
@@ -94,7 +94,7 @@ import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.ammolite.relays.Constants.activeTypesGlobalChats
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.nip01Core.relays.RelayStat
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
import kotlinx.coroutines.launch
@@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.quartz.nip01Core.relays.RelayStat
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LoadUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.LoadUser
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
@@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip01Core.relays.RelayStat
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
@Composable
@@ -44,7 +44,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
@@ -69,7 +68,7 @@ import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.CashuCardBorders
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.SmallishBorder
@@ -151,11 +150,7 @@ fun CashuPreviewNew(
val clipboardManager = LocalClipboardManager.current
Card(
modifier =
Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp)
.clip(shape = QuoteBorder),
modifier = CashuCardBorders,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -63,11 +63,11 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.njumpLink
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
@@ -77,7 +77,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip19Bech32.toNIP19
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun AnimatedBorderTextCornerRadius(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
textAlign: TextAlign? = null,
fontSize: TextUnit = 12.sp,
) {
val infiniteTransition = rememberInfiniteTransition()
val animatedFloatRestart =
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 100f,
animationSpec =
infiniteRepeatable(
animation = tween(5000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
)
Text(
text = text,
fontSize = fontSize,
modifier =
modifier
.drawBehind {
val brush =
Brush.sweepGradient(
colors = listOf(Color.Cyan, Color.Magenta, Color.Yellow),
)
drawRoundRect(
brush = brush,
style =
Stroke(
width = 2.dp.toPx(),
cap = StrokeCap.Round,
join = StrokeJoin.Round,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), animatedFloatRestart.value),
),
cornerRadius =
androidx.compose.ui.geometry
.CornerRadius(6.dp.toPx()),
)
}.padding(3.dp),
color = color,
textAlign = textAlign,
)
}
// Example usage in a composable function:
@Composable
@Preview
fun ExampleAnimatedBorder() {
Column {
AnimatedBorderTextCornerRadius(text = "Rounded Corners", Modifier)
}
}
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -62,6 +63,7 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
import com.vitorpamplona.amethyst.commons.richtext.Base64Segment
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
import com.vitorpamplona.amethyst.commons.richtext.CashuSegment
@@ -77,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.richtext.PhoneSegment
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment
import com.vitorpamplona.amethyst.commons.richtext.SecretEmoji
import com.vitorpamplona.amethyst.commons.richtext.Segment
import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment
import com.vitorpamplona.amethyst.model.HashtagIcon
@@ -91,13 +94,16 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LoadUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.LoadUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.theme.CashuCardBorders
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun isMarkdown(content: String): Boolean =
content.startsWith("> ") ||
@@ -375,6 +381,7 @@ private fun RenderWordWithoutPreview(
is WithdrawSegment -> Text(word.segmentText)
is CashuSegment -> Text(word.segmentText)
is EmailSegment -> ClickableEmail(word.segmentText)
is SecretEmoji -> Text(word.segmentText)
is PhoneSegment -> ClickablePhone(word.segmentText)
is BechSegment -> BechLink(word.segmentText, false, 0, backgroundColor, accountViewModel, nav)
is HashTagSegment -> HashTag(word, nav)
@@ -403,6 +410,7 @@ private fun RenderWordWithPreview(
is WithdrawSegment -> MayBeWithdrawal(word.segmentText, accountViewModel)
is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
is EmailSegment -> ClickableEmail(word.segmentText)
is SecretEmoji -> DisplaySecretEmoji(word, state, callbackUri, true, quotesLeft, backgroundColor, accountViewModel, nav)
is PhoneSegment -> ClickablePhone(word.segmentText)
is BechSegment -> BechLink(word.segmentText, true, quotesLeft, backgroundColor, accountViewModel, nav)
is HashTagSegment -> HashTag(word, nav)
@@ -510,6 +518,101 @@ fun DisplayFullNote(
}
}
@Composable
fun DisplaySecretEmoji(
segment: SecretEmoji,
state: RichTextViewerState,
callbackUri: String?,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
if (canPreview && quotesLeft > 0) {
var secretContent by remember {
mutableStateOf<RichTextViewerState?>(null)
}
var showPopup by remember {
mutableStateOf(false)
}
LaunchedEffect(segment) {
launch(Dispatchers.Default) {
secretContent =
CachedRichTextParser.parseText(
EmojiCoder.decode(segment.segmentText),
state.tags,
)
}
}
val localSecretContent = secretContent
AnimatedBorderTextCornerRadius(
segment.segmentText,
Modifier.clickable {
showPopup = !showPopup
},
)
if (localSecretContent != null && showPopup) {
CoreSecretMessage(localSecretContent, callbackUri, quotesLeft, backgroundColor, accountViewModel, nav)
}
} else {
Text(segment.segmentText)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CoreSecretMessage(
localSecretContent: RichTextViewerState,
callbackUri: String?,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
if (localSecretContent.paragraphs.size == 1) {
localSecretContent.paragraphs[0].words.forEach { word ->
RenderWordWithPreview(
word,
localSecretContent,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
}
} else if (localSecretContent.paragraphs.size > 1) {
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
Column(CashuCardBorders) {
localSecretContent.paragraphs.forEach { paragraph ->
FlowRow(
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
) {
paragraph.words.forEach { word ->
RenderWordWithPreview(
word,
localSecretContent,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
}
}
}
}
}
}
@Composable
fun HashTag(
segment: HashTagSegment,
@@ -0,0 +1,131 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.commons.hashtags.Lightning
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun SecretEmojiRequest(onSuccess: (String) -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp),
) {
Icon(
imageVector = CustomHashTagIcons.Lightning,
null,
modifier = Size20Modifier,
tint = Color.Unspecified,
)
Text(
text = stringRes(R.string.secret_emoji_maker),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp),
)
}
HorizontalDivider(thickness = DividerThickness)
var secretMessage by remember { mutableStateOf("") }
var publicPrefix by remember { mutableStateOf("") }
OutlinedTextField(
label = { Text(text = stringRes(R.string.secret_note_to_receiver)) },
modifier = Modifier.fillMaxWidth(),
value = secretMessage,
onValueChange = { secretMessage = it },
placeholder = {
Text(
text = stringRes(R.string.secret_note_to_receiver_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
singleLine = true,
)
OutlinedTextField(
label = { Text(text = stringRes(R.string.secret_visible_text)) },
modifier = Modifier.fillMaxWidth(),
value = publicPrefix,
onValueChange = { publicPrefix = it },
placeholder = {
Text(
text = stringRes(R.string.secret_visible_text_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
onSuccess(EmojiCoder.encode(publicPrefix, secretMessage))
},
shape = QuoteBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(
text = stringRes(R.string.secret_add_to_text),
color = Color.White,
fontSize = 20.sp,
)
}
}
@@ -0,0 +1,160 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
// COPIED FROM TEXT FIELD
// The only change is the contentPadding below
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThinPaddingTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
prefix: @Composable (() -> Unit)? = null,
suffix: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource? = null,
shape: Shape = TextFieldDefaults.shape,
colors: TextFieldColors = TextFieldDefaults.colors(),
// new fields
contentPadding: PaddingValues =
if (label == null) {
TextFieldDefaults.contentPaddingWithoutLabel(
start = 10.dp,
top = 12.dp,
end = 10.dp,
bottom = 12.dp,
)
} else {
TextFieldDefaults.contentPaddingWithLabel(
start = 10.dp,
top = 12.dp,
end = 10.dp,
bottom = 12.dp,
)
},
) {
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
// If color is not provided via the text style, use content color as a default
val textColor =
textStyle.color.takeOrElse {
val focused by interactionSource.collectIsFocusedAsState()
// this has changed, but only because of private access on the original
when {
!enabled -> MaterialTheme.colorScheme.placeholderText
isError -> MaterialTheme.colorScheme.onSurface
focused -> MaterialTheme.colorScheme.onSurface
else -> MaterialTheme.colorScheme.onSurface
}
}
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
BasicTextField(
value = value,
modifier =
modifier
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = 36.dp, // this has changed
),
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), // this has changed
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
decorationBox =
@Composable { innerTextField ->
TextFieldDefaults.DecorationBox(
value = value.text,
visualTransformation = visualTransformation,
innerTextField = innerTextField,
placeholder = placeholder,
label = label,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
prefix = prefix,
suffix = suffix,
supportingText = supportingText,
shape = shape,
singleLine = singleLine,
enabled = enabled,
isError = isError,
interactionSource = interactionSource,
colors = colors,
contentPadding = contentPadding, // this has changed
)
},
)
}
}
@@ -119,8 +119,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
import com.vitorpamplona.quartz.nip94FileMetadata.Dimension
import kotlinx.collections.immutable.ImmutableList
import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -208,10 +208,10 @@ fun VideoView(
roundedCorner: Boolean,
gallery: Boolean = false,
contentScale: ContentScale,
waveform: ImmutableList<Int>? = null,
waveform: WaveformTag? = null,
artworkUri: String? = null,
authorName: String? = null,
dimensions: Dimension? = null,
dimensions: DimensionTag? = null,
blurhash: String? = null,
nostrUriCallback: String? = null,
onDialog: ((Boolean) -> Unit)? = null,
@@ -239,10 +239,10 @@ fun VideoView(
thumb: VideoThumb? = null,
borderModifier: Modifier,
contentScale: ContentScale,
waveform: ImmutableList<Int>? = null,
waveform: WaveformTag? = null,
artworkUri: String? = null,
authorName: String? = null,
dimensions: Dimension? = null,
dimensions: DimensionTag? = null,
blurhash: String? = null,
nostrUriCallback: String? = null,
onDialog: ((Boolean) -> Unit)? = null,
@@ -355,7 +355,7 @@ fun VideoViewInner(
showControls: Boolean = true,
contentScale: ContentScale,
borderModifier: Modifier,
waveform: ImmutableList<Int>? = null,
waveform: WaveformTag? = null,
artworkUri: String? = null,
authorName: String? = null,
nostrUriCallback: String? = null,
@@ -732,7 +732,7 @@ private fun RenderVideoPlayer(
showControls: Boolean = true,
contentScale: ContentScale,
nostrUriCallback: String?,
waveform: ImmutableList<Int>? = null,
waveform: WaveformTag? = null,
keepPlaying: MutableState<Boolean>,
automaticallyStartPlayback: State<Boolean>,
activeOnScreen: MutableState<Boolean>,
@@ -863,7 +863,7 @@ private fun pollCurrentDuration(controller: MediaController) =
@Composable
fun Waveform(
waveform: ImmutableList<Int>,
waveform: WaveformTag,
controller: MediaController,
modifier: Modifier,
) {
@@ -897,13 +897,13 @@ fun Waveform(
@Composable
fun DrawWaveform(
waveform: ImmutableList<Int>,
waveform: WaveformTag,
waveformProgress: MutableFloatState,
modifier: Modifier,
) {
AudioWaveformReadOnly(
modifier = modifier.padding(start = 10.dp, end = 10.dp),
amplitudes = waveform,
amplitudes = waveform.wave,
progress = waveformProgress.floatValue,
progressBrush =
Brush.infiniteLinearGradient(
@@ -308,6 +308,7 @@ private fun saveMediaToGallery(
MediaSaverToDisk.downloadAndSave(
content.url,
mimeType = content.mimeType,
forceProxy = useTor,
localContext,
onSuccess = {
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.components
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.clickable
@@ -93,11 +94,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size30dp
import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip94FileMetadata.Dimension
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.utils.sha256.sha256
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
@@ -264,7 +265,9 @@ fun LocalImageView(
)
}
} else {
DisplayUrlWithLoadingSymbol(content)
WaitAndDisplay {
DisplayUrlWithLoadingSymbol(content)
}
}
}
is AsyncImagePainter.State.Error -> {
@@ -370,7 +373,9 @@ fun UrlImageView(
)
}
} else {
DisplayUrlWithLoadingSymbol(content)
WaitAndDisplay {
DisplayUrlWithLoadingSymbol(content)
}
}
}
is AsyncImagePainter.State.Error -> {
@@ -514,12 +519,33 @@ fun ShowHash(content: MediaUrlContent) {
verifiedHash?.let { HashVerificationSymbol(it) }
}
fun aspectRatio(dim: Dimension?): Float? {
fun aspectRatio(dim: DimensionTag?): Float? {
if (dim == null) return null
return dim.width.toFloat() / dim.height.toFloat()
}
@Composable
fun WaitAndDisplay(
content:
@Composable()
(AnimatedVisibilityScope.() -> Unit),
) {
val visible = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(200)
visible.value = true
}
AnimatedVisibility(
visible = visible.value,
enter = fadeIn(),
exit = fadeOut(),
content = content,
)
}
@Composable
fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) {
val uri = LocalUriHandler.current
@@ -641,7 +667,7 @@ fun ShareImageAction(
videoUri: String?,
postNostrUri: String?,
blurhash: String?,
dim: Dimension?,
dim: DimensionTag?,
hash: String?,
mimeType: String?,
onDismiss: () -> Unit,
@@ -679,7 +705,7 @@ fun ShareImageAction(
if (videoUri != null) {
val n19 = Nip19Parser.uriToRoute(postNostrUri)?.entity as? NEvent
if (n19 != null) {
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay[0], blurhash, dim, hash, mimeType) // TODO Whole list or first?
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType) // TODO Whole list or first?
accountViewModel.toast(R.string.media_added, R.string.media_added_to_profile_gallery)
}
}
@@ -695,7 +721,7 @@ private suspend fun verifyHash(content: MediaUrlContent): Boolean? {
if (content.hash == null) return null
Amethyst.instance.coilCache.openSnapshot(content.url)?.use { snapshot ->
val hash = CryptoUtils.sha256(snapshot.data.toFile().readBytes()).toHexKey()
val hash = sha256(snapshot.data.toFile().readBytes()).toHexKey()
Log.d("Image Hash Verification", "$hash == ${content.hash}")
@@ -48,8 +48,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink
import com.vitorpamplona.amethyst.ui.theme.Font17SP
import com.vitorpamplona.amethyst.ui.theme.Size17Modifier
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
@@ -57,11 +55,12 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import kotlinx.coroutines.runBlocking
class MarkdownMediaRenderer(
val startOfText: String,
val tags: ImmutableListOfLists<String>?,
val imetaByUrl: Map<String, IMetaTag>,
val canPreview: Boolean,
val quotesLeft: Int,
val backgroundColor: MutableState<Color>,
@@ -94,7 +93,7 @@ class MarkdownMediaRenderer(
val content =
parser.createMediaContent(
fullUrl = uri,
eventTags = tags ?: EmptyTagList,
eventTags = imetaByUrl,
description = title?.ifEmpty { null } ?: startOfText,
) ?: MediaUrlImage(url = uri, description = title?.ifEmpty { null } ?: startOfText)
@@ -116,7 +115,7 @@ class MarkdownMediaRenderer(
uri: String,
richTextStringBuilder: RichTextString.Builder,
) {
val content = parser.createMediaContent(uri, eventTags = tags ?: EmptyTagList, startOfText, callbackUri)
val content = parser.createMediaContent(uri, imetaByUrl, startOfText, callbackUri)
if (canPreview) {
if (content != null) {
@@ -49,10 +49,11 @@ import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
import com.vitorpamplona.amethyst.ui.uriToRoute
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip92IMeta.imetasByUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -93,7 +94,7 @@ fun RenderContentAsMarkdown(
remember(content) {
MarkdownMediaRenderer(
startOfText = content.take(100),
tags = tags,
imetaByUrl = tags?.lists?.imetasByUrl() ?: emptyMap(),
canPreview = canPreview,
quotesLeft = quotesLeft,
backgroundColor = backgroundColor,
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip17Dm.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
class ChatroomFeedFilter(
val withUser: ChatroomKey,
@@ -25,9 +25,9 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.relays.updated
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds
import com.vitorpamplona.quartz.nip17Dm.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
class ChatroomListKnownFeedFilter(
val account: Account,
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.relays.updated
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.ChatroomKeyable
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
class ChatroomListNewFeedFilter(
val account: Account,
@@ -24,9 +24,9 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
class CommunityFeedFilter(
val note: AddressableNote,
@@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.IsInPublicChatChannel
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
@@ -85,7 +85,7 @@ open class DiscoverChatFeedFilter(
null
}
} else if (noteEvent is IsInPublicChatChannel) {
val channel = noteEvent.channel()?.let { LocalCache.checkGetOrCreateNote(it) }
val channel = noteEvent.channelId()?.let { LocalCache.checkGetOrCreateNote(it) }
val channelEvent = channel?.event
if (channel != null &&
@@ -23,12 +23,11 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip19Bech32.parseAtagUnckecked
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
open class DiscoverCommunityFeedFilter(
val account: Account,
@@ -54,7 +53,7 @@ open class DiscoverCommunityFeedFilter(
val notes =
LocalCache.addressables.mapNotNullIntoSet { key, note ->
val noteEvent = note.event
if (noteEvent == null && shouldInclude(ATag.parseAtagUnckecked(key), filterParams)) {
if (noteEvent == null && shouldInclude(Address.parse(key), filterParams)) {
// send unloaded communities to the screen
note
} else if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) {
@@ -86,7 +85,7 @@ open class DiscoverCommunityFeedFilter(
if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) {
listOf(note)
} else if (noteEvent is CommunityPostApprovalEvent) {
noteEvent.communities().mapNotNull {
noteEvent.communityAddresses().mapNotNull {
val definitionNote = LocalCache.getOrCreateAddressableNote(it)
val definitionEvent = definitionNote.event
@@ -106,7 +105,7 @@ open class DiscoverCommunityFeedFilter(
}
private fun shouldInclude(
aTag: ATag?,
aTag: Address?,
params: FilterByListParams,
) = aTag != null && aTag.kind == CommunityDefinitionEvent.KIND && params.match(aTag)
@@ -26,10 +26,8 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_ENDED
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_LIVE
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent.Companion.STATUS_PLANNED
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
open class DiscoverLiveFeedFilter(
val account: Account,
@@ -93,9 +91,9 @@ open class DiscoverLiveFeedFilter(
fun convertStatusToOrder(status: String?): Int =
when (status) {
STATUS_LIVE -> 2
STATUS_PLANNED -> 1
STATUS_ENDED -> 0
StatusTag.STATUS.LIVE.code -> 2
StatusTag.STATUS.PLANNED.code -> 1
StatusTag.STATUS.ENDED.code -> 0
else -> 0
}
}
@@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.utils.TimeUtils
open class DiscoverNIP89FeedFilter(
@@ -76,7 +76,7 @@ open class DiscoverNIP89FeedFilter(
val filterParams = buildFilterParams(account)
return noteEvent.appMetaData()?.subscription != true &&
filterParams.match(noteEvent) &&
noteEvent.includeKind("5300") &&
noteEvent.includeKind(5300) &&
noteEvent.createdAt > TimeUtils.now() - lastAnnounced // && params.match(noteEvent)
}
@@ -24,14 +24,14 @@ import com.vitorpamplona.amethyst.model.AROUND_ME
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHashes
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.utils.TimeUtils
class FilterByListParams(
@@ -68,10 +68,10 @@ class FilterByListParams(
}
}
fun isATagInList(aTag: ATag): Boolean {
fun isAuthorInFollows(address: Address): Boolean {
if (followLists == null) return false
return aTag.pubKeyHex in followLists.authors
return address.pubKeyHex in followLists.authors
}
fun match(
@@ -81,10 +81,10 @@ class FilterByListParams(
(isHiddenList || isNotHidden(noteEvent.pubKey)) &&
isNotInTheFuture(noteEvent)
fun match(aTag: ATag?) =
aTag != null &&
(isGlobal || isATagInList(aTag)) &&
(isHiddenList || isNotHidden(aTag.pubKeyHex))
fun match(address: Address?) =
address != null &&
(isGlobal || isAuthorInFollows(address)) &&
(isHiddenList || isNotHidden(address.pubKeyHex))
companion object {
fun showHiddenKey(
@@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
class GeoHashFeedFilter(
@@ -23,15 +23,15 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
class HashtagFeedFilter(
@@ -27,10 +27,10 @@ import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
class HomeConversationsFeedFilter(
val account: Account,
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -23,23 +23,25 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.experimental.forks.forkFromVersion
import com.vitorpamplona.quartz.experimental.forks.isForkFromAddressWithPubkey
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip34Git.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.GitPatchEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
@@ -138,13 +140,13 @@ class NotificationFeedFilter(
return true
}
if (event is BaseTextNoteEvent) {
if (event is BaseThreadedEvent) {
if (note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) return true
val isAuthoredPostCited = event.findCitations().any { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex }
val isAuthorDirectlyCited = event.citedUsers().contains(authorHex)
val isAuthorOfAFork =
event.isForkFromAddressWithPubkey(authorHex) || (event.forkFromVersion()?.let { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } == true)
event.isForkFromAddressWithPubkey(authorHex) || (event.forkFromVersion()?.let { LocalCache.getNoteIfExists(it.eventId)?.author?.pubkeyHex == authorHex } == true)
return isAuthoredPostCited || isAuthorDirectlyCited || isAuthorOfAFork
}

Some files were not shown because too many files have changed in this diff Show More