diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 217e5c51f..fdf8d994a 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 609a10005..2daee8c53 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' - id 'org.jlleitschuh.gradle.ktlint' version "11.3.1" + id 'org.jlleitschuh.gradle.ktlint' version "11.5.0" id 'com.google.gms.google-services' } @@ -20,7 +20,7 @@ android { vectorDrawables { useSupportLibrary true } - resConfigs("ar", "cs", "de", "eo", "es", "fa", "fr", "hu", "night", "nl", "pt-rBR", "ru", "sv-rSE", "ta", "tr", "uk", "zh", "sh-rHK", "zh-rTW", "ja") + resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW', 'ja'] } buildTypes { @@ -37,7 +37,7 @@ android { } } - flavorDimensions "channel" + flavorDimensions = ["channel"] productFlavors { play { @@ -73,7 +73,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion "1.4.7" + kotlinCompilerExtensionVersion "1.5.1" } packagingOptions { @@ -96,7 +96,7 @@ dependencies { implementation "androidx.fragment:fragment-ktx:$fragment_version" // Navigation - implementation("androidx.navigation:navigation-compose:$nav_version") + implementation "androidx.navigation:navigation-compose:$nav_version" // Observe Live data as State implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version" @@ -108,7 +108,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "net.engawapg.lib:zoomable:1.4.3" @@ -153,7 +153,7 @@ dependencies { implementation "io.github.url-detector:url-detector:0.1.23" // For QR generation - implementation 'com.google.zxing:core:3.5.1' + implementation 'com.google.zxing:core:3.5.2' implementation 'com.journeyapps:zxing-android-embedded:4.3.0' // Markdown @@ -196,16 +196,13 @@ dependencies { // LibSodium for XChaCha encryption implementation "com.goterl:lazysodium-android:5.1.0@aar" - implementation "net.java.dev.jna:jna:5.12.1@aar" + implementation 'net.java.dev.jna:jna:5.13.0@aar' // Video compression lib implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1' // Image compression lib implementation 'id.zelory:compressor:3.0.1' - // Automatic memory leak detection - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' - testImplementation 'junit:junit:4.13.2' testImplementation 'io.mockk:mockk:1.13.5' androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha01' diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/GiftWrapEventTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/GiftWrapEventTest.kt index fbda04af0..e349e0339 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/GiftWrapEventTest.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/GiftWrapEventTest.kt @@ -499,13 +499,46 @@ class GiftWrapEventTest { val gossip = unwrapUnsealGossip(json, privateKey) assertEquals("asdfasdfasdf", gossip?.content) - assertEquals(0L, gossip?.createdAt) + assertEquals(1690659269L, gossip?.createdAt) assertEquals("827ba09d32ab81d62c60f657b350198c8aaba84372dab9ad3f4f6b8b7274b707", gossip?.id) assertEquals(14, gossip?.kind) assertEquals("subject", gossip?.tags?.firstOrNull()?.get(0)) assertEquals("test", gossip?.tags?.firstOrNull()?.get(1)) } + @Test + fun testFromCoracle3() { + val json = """ +{ + "content": "{\"ciphertext\":\"PGCodiacmCB/sClw0C6DQRpP/XIfNAKCVZdKLQpOqytgbFryhs9Z+cPldq2FajXzxs3jQsF7/EVQyWpuV8pXetvFT9tvzjg4Xcm7ZcooLUnAeAo2xZNcJontN4cGubuDqKuXy5n59yXP1fIxfnJxRTRRdCZ2edhsKeNR5NSByUi+StjV10rnfHt8AhZCpiXiZ/giTOsC4wdaeONPgMzMeljaJWLvl6n11VjmXhkx1mXIQt43CNB1hIqO3p89Mbd9p+nlLrOsR+Xs0TB4DCh4XTPbvgf7B7Z+PgOfl3GZfJy9x6TciLcF4E3Ba1zrPe4f79czCIEiJ1yrIKrzzYvv+it35DZQ8fgveFXpyHnNL29hml8PNjyOsFbCHVYLMGw88evI5PijOcpe1TtdoioX8kX5kVEQSKJXuoSjTorvbRPCgGzaa1m0J0uTpzri5VD22a/Jh2CcAnubg6w4JDdUWCogdSV3NqiJllo7ZF7WnZ3apPdRD23MEfphVBJrcLBUNlmwajnY5IvVTKTkZOP50r9dBapvMWXIo6M6zhy/5vVWJz57863pelYCRG4upaXZuNK9sMBtbiphxmFR83i8RML8KN8Q391Cd/xBN7TxJNo5p2YU25VeGZUAmHY8DYlMQDm8Br0nStAXp3T+DzTRL8FTECa8DJV+KTAPoCxqhv3B28Ehr0XAP75CsHoLU00G48cR7h3vQ0CnfKh6KXU6nnDA5OWfpMYpirACCpsnpSD0OaCQ3gkQp3zZNMS3HcOpnPK/IY7R0esbzgAkvNhkyxaIfPDdf+eRUSOA9+2Ji28MwjjY8Dw3SLdUqCOzIDjQeR/T5oNmaQJm3lZ8G0FxxC6ejD4VJX/NI/x+STeB9jWHWmHZvqKzV6JHNh6qmZb6TKSIPOHpafWFoeJFOmiiigf46sju9vRXmVEAx59HXWnvnvCBNJg877yCMulB6xyQuSdVDuotQU4tQZwCKedTHJ6GqjesM98UlJrDtdWQURwwW1qc7N8tS6PukmUVEf0jmbIWVIBmUlkcVuiSs1g1h1kjt8c4MnGTz3CSgpOd1MqxLrl9WwrTqM+YnE+yeZYUjFoewyKZIQ==\",\"nonce\":\"OdCZczJiUGR4bOGIElQ4UUH4dQmG5U/3\",\"v\":1}", + "kind": 1059, + "created_at": 1690772945, + "pubkey": "e01475e87896800b7285eb0daf263c59f811c8fc5bc8daa105d2c98b6d7c4952", + "tags": [ + [ + "p", + "b08d8857a92b4d6aa580ff55cc3c18c4edf313c83388c34abc118621f74f1a78" + ] + ], + "id": "d9fc85ece892ce45ffa737b3ddc0f8b752623181d75363b966191f8c03d2debe", + "sig": "1b20416b83f4b5b8eead11e29c185f46b5e76d1960e4505210ddd00f7a6973cc11268f52a8989e3799b774d5f3a55db95bed4d66a1b6e88ab54becec5c771c17" +} + """.trimIndent() + + val privateKey = "7dd22cafc512c0bc363a259f6dcda515b13ae3351066d7976fd0bb79cbd0d700" + + val gossip = unwrapUnsealGossip(json, privateKey) + + assertEquals("8d1a56008d4e31dae2fb8bef36b3efea519eff75f57033107e2aa16702466ef2", gossip?.id) + assertEquals("Howdy", gossip?.content) + assertEquals(1690833960L, gossip?.createdAt) + assertEquals(14, gossip?.kind) + assertEquals("p", gossip?.tags?.firstOrNull()?.get(0)) + assertEquals("b08d8857a92b4d6aa580ff55cc3c18c4edf313c83388c34abc118621f74f1a78", gossip?.tags?.firstOrNull()?.get(1)) + assertEquals("subject", gossip?.tags?.getOrNull(1)?.get(0)) + assertEquals("Stuff", gossip?.tags?.getOrNull(1)?.get(1)) + } + fun unwrapUnsealGossip(json: String, privateKey: HexKey): Gossip? { val pkBytes = privateKey.hexToByteArray() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index c3ff33225..44eecf7fa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -117,7 +117,7 @@ object ServiceManager { LocalCache.pruneOldAndHiddenMessages(it) LocalCache.pruneHiddenMessages(it) LocalCache.pruneContactLists(it) - // LocalCache.pruneNonFollows(it) + LocalCache.pruneRepliesAndReactions(it) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 7e4d04904..70cbd2aa5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1110,6 +1110,23 @@ object LocalCache { refreshObservers(note) } + private fun consume(event: ChatMessageEvent, relay: Relay?) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + private fun consume(event: SealedGossipEvent, relay: Relay?) { val note = getOrCreateNote(event.id) val author = getOrCreateUser(event.pubKey) @@ -1219,13 +1236,12 @@ object LocalCache { } return notes.values.filter { - (it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false) || - (it.event is PollNoteEvent && it.event?.content()?.contains(text, true) ?: false) || - (it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false) || + it.event?.content()?.contains(text, true) ?: false || + it.event?.matchTag1With(text) ?: false || it.idHex.startsWith(text, true) || it.idNote().startsWith(text, true) } + addressables.values.filter { - (it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false || + it.event?.content()?.contains(text, true) ?: false || it.event?.matchTag1With(text) ?: false || it.idHex.startsWith(text, true) } @@ -1256,6 +1272,10 @@ object LocalCache { it.value.clearLive() } + addressables.forEach { + it.value.clearLive() + } + users.forEach { it.value.clearLive() } @@ -1267,6 +1287,8 @@ object LocalCache { channels.forEach { it -> val toBeRemoved = it.value.pruneOldAndHiddenMessages(account) + val childrenToBeRemoved = mutableListOf() + toBeRemoved.forEach { notes.remove(it.idHex) // Doesn't need to clean up the replies and mentions.. Too small to matter. @@ -1275,17 +1297,84 @@ object LocalCache { it.replyTo?.forEach { _ -> it.removeReply(it) } + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) } + removeChildrenOf(childrenToBeRemoved) + if (toBeRemoved.size > 100 || it.value.notes.size > 100) { println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size} kept") } } + + users.forEach { userPair -> + userPair.value.privateChatrooms.values.map { + val toBeRemoved = it.pruneMessagesToTheLatestOnly() + + val childrenToBeRemoved = mutableListOf() + + toBeRemoved.forEach { + notes.remove(it.idHex) + + // Counts the replies + it.replyTo?.forEach { _ -> + it.removeReply(it) + } + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) + } + + removeChildrenOf(childrenToBeRemoved) + + if (toBeRemoved.size > 1) { + println("PRUNE: ${toBeRemoved.size} private messages with ${userPair.value.toBestDisplayName()} removed. ${it.roomMessages.size} kept") + } + } + } + } + + fun pruneRepliesAndReactions(account: Account) { + checkNotInMainThread() + val user = account.userProfile() + + val toBeRemoved = notes.filter { + ( + (it.value.event is TextNoteEvent && !it.value.isNewThread()) || + it.value.event is ReactionEvent || it.value.event is LnZapEvent || it.value.event is LnZapRequestEvent || + it.value.event is ReportEvent || it.value.event is GenericRepostEvent + ) && + it.value.liveSet?.isInUse() != true && // don't delete if observing. + it.value.author != user && // don't delete if it is the logged in account + it.value.event?.isTaggedUser(user.pubkeyHex) != true // don't delete if it's a notification to the logged in user + }.values + + val childrenToBeRemoved = mutableListOf() + + toBeRemoved.forEach { + notes.remove(it.idHex) + + it.replyTo?.forEach { masterNote -> + masterNote.removeReply(it) + masterNote.removeBoost(it) + masterNote.removeReaction(it) + masterNote.removeZap(it) + it.clearEOSE() // allows reloading of these events + } + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) + } + + if (toBeRemoved.size > 1) { + println("PRUNE: ${toBeRemoved.size} thread replies removed.") + } } fun pruneHiddenMessages(account: Account) { checkNotInMainThread() + val childrenToBeRemoved = mutableListOf() + val toBeRemoved = account.hiddenUsers.map { userHex -> ( notes.values.filter { @@ -1307,11 +1396,30 @@ object LocalCache { } notes.remove(it.idHex) + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) } + removeChildrenOf(childrenToBeRemoved) + println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden") } + fun removeChildrenOf(nextToBeRemoved: List) { + nextToBeRemoved.forEach { note -> + if (note.event is LnZapEvent) { + (note.event as LnZapEvent).zappedAuthor().mapNotNull { getUserIfExists(it)?.removeZap(note) } + } + if (note.event is LnZapRequestEvent) { + (note.event as LnZapRequestEvent).zappedAuthor().mapNotNull { getUserIfExists(it)?.removeZap(note) } + } + if (note.event is ReportEvent) { + (note.event as ReportEvent).reportedAuthor().mapNotNull { getUserIfExists(it.key)?.removeZap(note) } + } + notes.remove(note.idHex) + } + } + fun pruneContactLists(userAccount: Account) { checkNotInMainThread() @@ -1359,6 +1467,7 @@ object LocalCache { is ChannelMessageEvent -> consume(event, relay) is ChannelMetadataEvent -> consume(event) is ChannelMuteUserEvent -> consume(event) + is ChatMessageEvent -> consume(event, relay) is ClassifiedsEvent -> consume(event) is CommunityDefinitionEvent -> consume(event, relay) is CommunityPostApprovalEvent -> { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 5448367b5..d09483d37 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -158,6 +158,35 @@ open class Note(val idHex: String) { boosts = boosts - note liveSet?.boosts?.invalidateData() } + + fun removeAllChildNotes(): Set { + val toBeRemoved = replies + + reactions.values.flatten() + + boosts + + reports.values.flatten() + + zaps.keys + + zaps.values.filterNotNull() + + zapPayments.keys + + zapPayments.values.filterNotNull() + + replies = setOf() + reactions = mapOf>() + boosts = setOf() + reports = mapOf>() + zaps = mapOf() + zapPayments = mapOf() + relays = setOf() + lastReactionsDownloadTime = emptyMap() + + liveSet?.replies?.invalidateData() + liveSet?.reactions?.invalidateData() + liveSet?.boosts?.invalidateData() + liveSet?.reports?.invalidateData() + liveSet?.zaps?.invalidateData() + + return toBeRemoved + } + fun removeReaction(note: Note) { val tags = note.event?.tags() ?: emptyList() val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+" @@ -554,6 +583,10 @@ open class Note(val idHex: String) { zaps = emptyMap() } + fun clearEOSE() { + lastReactionsDownloadTime = emptyMap() + } + var liveSet: NoteLiveSet? = null fun live(): NoteLiveSet { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt index 348a46837..18a763647 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt @@ -1,13 +1,17 @@ package com.vitorpamplona.amethyst.model object TimeUtils { - const val fiveMinutes = 60 * 5 - const val oneHour = 60 * 60 - const val oneDay = 24 * 60 * 60 + const val oneMinute = 60 + const val fiveMinutes = 5 * oneMinute + const val oneHour = 60 * oneMinute + const val eightHours = 8 * oneHour + const val oneDay = 24 * oneHour + const val oneWeek = 7 * oneDay fun now() = System.currentTimeMillis() / 1000 fun fiveMinutesAgo() = now() - fiveMinutes fun oneHourAgo() = now() - oneHour fun oneDayAgo() = now() - oneDay - fun eightHoursAgo() = now() - (oneHour * 8) + fun eightHoursAgo() = now() - eightHours + fun oneWeekAgo() = now() - oneWeek } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index e8e4dc59a..cec48cd70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -137,6 +137,16 @@ class User(val pubkeyHex: String) { } } + fun removeZap(zapRequestOrZapEvent: Note) { + if (zapRequestOrZapEvent in zaps.keys) { + zaps = zaps.minus(zapRequestOrZapEvent) + liveSet?.zaps?.invalidateData() + } else if (zapRequestOrZapEvent in zaps.values) { + zaps = zaps.filter { it.value != zapRequestOrZapEvent } + liveSet?.zaps?.invalidateData() + } + } + fun zappedAmount(): BigDecimal { return zaps.mapNotNull { it.value?.event } .filterIsInstance() @@ -383,6 +393,22 @@ class Chatroom() { roomMessages = roomMessages + msg } } + + fun pruneMessagesToTheLatestOnly(): Set { + val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + + val toKeep = if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) { + // Recent messages, keep last 100 + sorted.take(100).toSet() + } else { + // Old messages, keep the last one. + sorted.take(1).toSet() + } + sorted.filter { it.liveSet?.isInUse() ?: false } + + val toRemove = roomMessages.minus(toKeep) + roomMessages = toKeep + return toRemove + } } @Stable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt index cfda4b271..7645a12f4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt @@ -6,7 +6,6 @@ import android.location.Geocoder import android.location.Location import android.location.LocationListener import android.location.LocationManager -import android.os.Bundle import android.os.HandlerThread import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @@ -53,9 +52,6 @@ class LocationUtil(context: Context) { locationStateFlow.value = location } - override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { - } - override fun onProviderEnabled(provider: String) { providerState.value = true } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt index 48b60c33d..94879c6d1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.model.GiftWrapEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter @@ -24,7 +25,7 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") { TypedFilter( types = setOf(FeedType.PRIVATE_DMS), filter = JsonFilter( - kinds = listOf(PrivateDmEvent.kind), + kinds = listOf(PrivateDmEvent.kind, GiftWrapEvent.kind), authors = listOf(myPeer.pubkeyHex), tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index 5e0f92f44..309e63044 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent +import com.vitorpamplona.amethyst.service.model.GiftWrapEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.amethyst.service.relays.EOSEAccount @@ -20,7 +21,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") { fun createMessagesToMeFilter() = TypedFilter( types = setOf(FeedType.PRIVATE_DMS), filter = JsonFilter( - kinds = listOf(PrivateDmEvent.kind), + kinds = listOf(PrivateDmEvent.kind, GiftWrapEvent.kind), tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt index 4341e0462..3e04dee9e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt @@ -1,6 +1,10 @@ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent +import com.vitorpamplona.amethyst.service.model.HighlightEvent +import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent @@ -22,7 +26,7 @@ object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") { hashToLoad ) ), - kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind), + kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind), limit = 200 ) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt index 9b278e09c..94784f29a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt @@ -1,7 +1,11 @@ package com.vitorpamplona.amethyst.service import androidx.compose.ui.text.capitalize +import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent +import com.vitorpamplona.amethyst.service.model.HighlightEvent +import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent @@ -26,7 +30,7 @@ object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") { hashToLoad.capitalize() ) ), - kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind), + kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind), limit = 200 ) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index 76934bd34..51ae523f1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -9,7 +9,7 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter import fr.acinq.secp256k1.Hex -object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") { +object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") { private var searchString: String? = null private fun createAnythingWithIDFilter(): List? { @@ -54,6 +54,28 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") { TypedFilter( types = setOf(FeedType.SEARCH), filter = JsonFilter( + kinds = listOf( + TextNoteEvent.kind, LongTextNoteEvent.kind, BadgeDefinitionEvent.kind, + PeopleListEvent.kind, BookmarkListEvent.kind, AudioTrackEvent.kind, PinListEvent.kind, + PollNoteEvent.kind, ChannelCreateEvent.kind + ), + search = mySearchString, + limit = 100 + ) + ), + TypedFilter( + types = setOf(FeedType.SEARCH), + filter = JsonFilter( + kinds = listOf( + ChannelMetadataEvent.kind, + ClassifiedsEvent.kind, + CommunityDefinitionEvent.kind, + EmojiPackEvent.kind, + HighlightEvent.kind, + LiveActivitiesEvent.kind, + PollNoteEvent.kind, + NNSEvent.kind + ), search = mySearchString, limit = 100 ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index c09d38206..c5274166c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -12,7 +12,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { private var eventsToWatch = setOf() private var addressesToWatch = setOf() - private fun createTagToAddressFilter(): List? { + private fun createReactionsToWatchInAddressFilter(): List? { val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch if (addressesToWatch.isEmpty()) { @@ -25,12 +25,15 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { types = COMMON_FEED_TYPES, filter = JsonFilter( kinds = listOf( - TextNoteEvent.kind, LongTextNoteEvent.kind, - ReactionEvent.kind, RepostEvent.kind, GenericRepostEvent.kind, ReportEvent.kind, - LnZapEvent.kind, LnZapRequestEvent.kind, - BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind, - PollNoteEvent.kind, AudioTrackEvent.kind, PinListEvent.kind, - PeopleListEvent.kind, BookmarkListEvent.kind + TextNoteEvent.kind, + ReactionEvent.kind, + RepostEvent.kind, + GenericRepostEvent.kind, + ReportEvent.kind, + LnZapEvent.kind, + PollNoteEvent.kind, + CommunityPostApprovalEvent.kind, + LiveActivitiesChatMessageEvent.kind ), tags = mapOf("a" to listOf(aTag.toTag())), since = it.lastReactionsDownloadTime, @@ -55,7 +58,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { types = COMMON_FEED_TYPES, filter = JsonFilter( kinds = listOf(aTag.kind), - authors = listOf(aTag.pubKeyHex) + authors = listOf(aTag.pubKeyHex), + limit = 1000 // Max amount of "replies" to download on a specific event. ) ) } else { @@ -64,7 +68,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { filter = JsonFilter( kinds = listOf(aTag.kind), tags = mapOf("d" to listOf(aTag.dTag)), - authors = listOf(aTag.pubKeyHex) + authors = listOf(aTag.pubKeyHex), + limit = 1000 // Max amount of "replies" to download on a specific event. ) ) } @@ -85,17 +90,12 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { filter = JsonFilter( kinds = listOf( TextNoteEvent.kind, - LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, GenericRepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, - LnZapRequestEvent.kind, - PollNoteEvent.kind, - HighlightEvent.kind, - AudioTrackEvent.kind, - PinListEvent.kind + PollNoteEvent.kind ), tags = mapOf("e" to listOf(it.idHex)), since = it.lastReactionsDownloadTime, @@ -154,7 +154,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { val reactions = createRepliesAndReactionsFilter() val missing = createLoadEventsIfNotLoadedFilter() val addresses = createAddressFilter() - val addressReactions = createTagToAddressFilter() + val addressReactions = createReactionsToWatchInAddressFilter() singleEventChannel.typedFilters = listOfNotNull(missing, addresses, reactions, addressReactions).flatten().ifEmpty { null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt index 42fa69c69..5ea09edd7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt @@ -1,5 +1,7 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.toHexKey + class EventFactory { companion object { fun create( @@ -11,6 +13,34 @@ class EventFactory { content: String, sig: String, lenient: Boolean + ): Event { + val internedTags = tags.map { + it.map { + it.intern() + } + } + + return internedCreate( + id = id.intern(), + pubKey = pubKey.intern(), + createdAt = createdAt, + kind = kind, + tags = internedTags, + content = content, + sig = sig, + lenient = lenient + ) + } + + fun internedCreate( + id: String, + pubKey: String, + createdAt: Long, + kind: Int, + tags: List>, + content: String, + sig: String, + lenient: Boolean ) = when (kind) { AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig) AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig) @@ -24,6 +54,14 @@ class EventFactory { ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) + ChatMessageEvent.kind -> { + if (id.isBlank()) { + val id = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey() + ChatMessageEvent(id, pubKey, createdAt, tags, content, sig) + } else { + ChatMessageEvent(id, pubKey, createdAt, tags, content, sig) + } + } ClassifiedsEvent.kind -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig) CommunityDefinitionEvent.kind -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig) CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GiftWrapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GiftWrapEvent.kt index 1365dd3c2..e33cec26a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GiftWrapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GiftWrapEvent.kt @@ -19,13 +19,14 @@ class GiftWrapEvent( content: String, sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - private var innerEvent: Event? = null + private var cachedInnerEvent: Map = mapOf() - fun cachedInnerEvent(privKey: ByteArray): Event? { - if (innerEvent != null) return innerEvent + fun cachedGossip(privKey: ByteArray): Event? { + val hex = privKey.toHexKey() + if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex] val myInnerEvent = unwrap(privKey = privKey) - innerEvent = myInnerEvent + cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent) return myInnerEvent } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/SealedGossipEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/SealedGossipEvent.kt index 4e12cfdd5..0e51426c7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/SealedGossipEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/SealedGossipEvent.kt @@ -2,12 +2,14 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log import androidx.compose.runtime.Immutable +import com.google.gson.annotations.SerializedName import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.TimeUtils import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.CryptoUtils import com.vitorpamplona.amethyst.service.EncryptedInfo +import com.vitorpamplona.amethyst.service.relays.Client @Immutable class SealedGossipEvent( @@ -18,14 +20,16 @@ class SealedGossipEvent( content: String, sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - private var innerEvent: Gossip? = null + private var cachedInnerEvent: Map = mapOf() - fun cachedGossip(privKey: ByteArray): Gossip? { - if (innerEvent != null) return innerEvent + fun cachedGossip(privKey: ByteArray): Event? { + val hex = privKey.toHexKey() + if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex] - val myInnerEvent = unseal(privKey = privKey) - innerEvent = myInnerEvent - return myInnerEvent + val gossip = unseal(privKey = privKey) + val event = gossip?.mergeWith(this) + cachedInnerEvent = cachedInnerEvent + Pair(hex, event) + return event } fun unseal(privKey: ByteArray): Gossip? = try { @@ -88,14 +92,28 @@ class SealedGossipEvent( } } -open class Gossip( - val id: HexKey, - val pubKey: HexKey, - val createdAt: Long, - val kind: Int, - val tags: List>, - val content: String +class Gossip( + val id: HexKey?, + @SerializedName("pubkey") + val pubKey: HexKey?, + @SerializedName("created_at") + val createdAt: Long?, + val kind: Int?, + val tags: List>?, + val content: String? ) { + fun mergeWith(event: SealedGossipEvent): Event { + val newPubKey = pubKey?.ifBlank { null } ?: event.pubKey + val newCreatedAt = if (createdAt != null && createdAt > 1000) createdAt else event.createdAt + val newKind = kind ?: 0 + val newTags = (tags ?: emptyList()).plus(event.tags) + val newContent = content ?: "" + val newID = id?.ifBlank { null } ?: Event.generateId(newPubKey, newCreatedAt, newKind, newTags, newContent).toHexKey() + val sig = "" + + return EventFactory.create(newID, newPubKey, newCreatedAt, newKind, newTags, newContent, sig, Client.lenient) + } + companion object { fun create(event: Event): Gossip { return Gossip(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt index 5e772035b..7c7051b5c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt @@ -18,6 +18,8 @@ import com.vitorpamplona.amethyst.ui.MainActivity object NotificationUtils { private var dmChannel: NotificationChannel? = null private var zapChannel: NotificationChannel? = null + private const val DM_GROUP_KEY = "com.vitorpamplona.amethyst.DM_NOTIFICATION" + private const val ZAP_GROUP_KEY = "com.vitorpamplona.amethyst.ZAP_NOTIFICATION" private fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel { if (dmChannel != null) return dmChannel!! @@ -70,7 +72,7 @@ object NotificationUtils { val zapChannel = getOrCreateZapChannel(applicationContext) val channelId = applicationContext.getString(R.string.app_notification_zaps_channel_id) - sendNotification(id, messageBody, messageTitle, pictureUrl, uri, channelId, applicationContext) + sendNotification(id, messageBody, messageTitle, pictureUrl, uri, channelId, ZAP_GROUP_KEY, applicationContext) } fun NotificationManager.sendDMNotification( @@ -84,7 +86,7 @@ object NotificationUtils { val dmChannel = getOrCreateDMChannel(applicationContext) val channelId = applicationContext.getString(R.string.app_notification_dms_channel_id) - sendNotification(id, messageBody, messageTitle, pictureUrl, uri, channelId, applicationContext) + sendNotification(id, messageBody, messageTitle, pictureUrl, uri, channelId, DM_GROUP_KEY, applicationContext) } fun NotificationManager.sendNotification( @@ -94,6 +96,7 @@ object NotificationUtils { pictureUrl: String?, uri: String, channelId: String, + notificationGroupKey: String, applicationContext: Context ) { if (pictureUrl != null) { @@ -110,6 +113,7 @@ object NotificationUtils { picture = imageResult.drawable as? BitmapDrawable, uri = uri, channelId, + notificationGroupKey, applicationContext = applicationContext ) } else { @@ -120,6 +124,7 @@ object NotificationUtils { picture = null, uri = uri, channelId, + notificationGroupKey, applicationContext = applicationContext ) } @@ -132,6 +137,7 @@ object NotificationUtils { picture: BitmapDrawable?, uri: String, channelId: String, + notificationGroupKey: String, applicationContext: Context ) { val notId = id.hashCode() @@ -164,7 +170,8 @@ object NotificationUtils { .setContentTitle(messageTitle) .setContentText(applicationContext.getString(R.string.app_notification_private_message)) .setLargeIcon(picture?.bitmap) - .setGroup(messageTitle) + // .setGroup(messageTitle) + // .setGroup(notificationGroupKey) //-> Might need a Group summary as well before we activate this .setContentIntent(contentPendingIntent) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) @@ -178,7 +185,8 @@ object NotificationUtils { .setContentTitle(messageTitle) .setContentText(messageBody) .setLargeIcon(picture?.bitmap) - .setGroup(messageTitle) + // .setGroup(messageTitle) + // .setGroup(notificationGroupKey) //-> Might need a Group summary as well before we activate this .setContentIntent(contentPendingIntent) .setPublicVersion(builderPublic.build()) .setPriority(NotificationCompat.PRIORITY_HIGH) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt index eb26e9f49..0fbd4db7f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt @@ -323,7 +323,7 @@ fun loadRelayInfo( Toast .makeText( context, - context.getString(R.string.an_error_ocurred_trying_to_get_relay_information, dirtyUrl), + context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), Toast.LENGTH_SHORT ).show() } @@ -334,7 +334,7 @@ fun loadRelayInfo( Toast .makeText( context, - context.getString(R.string.an_error_ocurred_trying_to_get_relay_information, dirtyUrl), + context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), Toast.LENGTH_SHORT ).show() } @@ -348,7 +348,7 @@ fun loadRelayInfo( Toast .makeText( context, - context.getString(R.string.an_error_ocurred_trying_to_get_relay_information, dirtyUrl), + context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), Toast.LENGTH_SHORT ).show() } @@ -361,7 +361,7 @@ fun loadRelayInfo( Toast .makeText( context, - context.getString(R.string.an_error_ocurred_trying_to_get_relay_information, dirtyUrl), + context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), Toast.LENGTH_SHORT ).show() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt index 79f546610..46fe688d0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt @@ -120,7 +120,7 @@ fun RelaySelectionDialog( val selectedRelays = relays.filter { it.isSelected } if (selectedRelays.isEmpty()) { scope.launch { - Toast.makeText(context, "Select a relay to continue", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.select_a_relay_to_continue), Toast.LENGTH_SHORT).show() } return@PostButton } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt index 34661474a..9405b7903 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt @@ -149,7 +149,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) { scope.launch { Toast.makeText( context, - "No Lightning Address set", + context.getString(R.string.no_lightning_address_set), Toast.LENGTH_SHORT ).show() } @@ -180,7 +180,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) { var orignaltoken = token.token clipboardManager.setText(AnnotatedString("$orignaltoken")) scope.launch { - Toast.makeText(context, "Copied token to clipboard", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.copied_token_to_clipboard), Toast.LENGTH_SHORT).show() } } }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index ab0cf1881..90f2b2f02 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -25,17 +25,17 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() } override fun feed(): List { - val notes = innerApplyFilter(LocalCache.notes.values) - val longFormNotes = innerApplyFilter(LocalCache.addressables.values) + val notes = innerApplyFilter(LocalCache.notes.values, true) + val longFormNotes = innerApplyFilter(LocalCache.addressables.values, false) return sort(notes + longFormNotes) } override fun applyFilter(collection: Set): Set { - return innerApplyFilter(collection) + return innerApplyFilter(collection, false) } - private fun innerApplyFilter(collection: Collection): Set { + private fun innerApplyFilter(collection: Collection, ignoreAddressables: Boolean): Set { val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS val isHiddenList = showHiddenKey() @@ -52,6 +52,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() .filter { it -> val noteEvent = it.event (noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) && + (!ignoreAddressables || noteEvent.kind() < 10000) && (isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeoSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable (isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) && diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index b8e0d82b0..9389d7164 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -511,7 +511,13 @@ fun debugState(context: Context) { Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB") Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size) + Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size) Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size) - Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size) + LocalCache.notes.values.groupBy { it.event?.kind() }.forEach { + Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ") + } + LocalCache.addressables.values.groupBy { it.event?.kind() }.forEach { + Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ") + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index aac3c8cb7..bf34dba7f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1015,7 +1015,7 @@ fun DisplayLNAddress( scope.launch { Toast.makeText( context, - "Payment Successful", // Turn this into a UI animation + context.getString(R.string.payment_successful), // Turn this into a UI animation Toast.LENGTH_LONG ).show() } @@ -1025,7 +1025,7 @@ fun DisplayLNAddress( context, response.error?.message ?: response.error?.code?.toString() - ?: "Error parsing error message", + ?: context.getString(R.string.error_parsing_error_message), Toast.LENGTH_LONG ).show() } diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 3799d2b5b..a5aa1b489 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -47,6 +47,8 @@ " a " "v kanálu " Banner profilu + Platba úspěšná + Chyba při zpracování chybové zprávy " Sleduje" " Sledující" Profil @@ -412,7 +414,7 @@ Zapraiser na %1$s. Do cíle zbývá %2$s sats Číst z Relay Zapisovat do Relay - Při pokusu o získání informací z Relay se vyskytla chyba z %1$s + Při pokusu o získání informací z Relay se vyskytla chyba z %1$s Vlastník Verze Software @@ -437,6 +439,8 @@ Platba Cashu Token Vyměnit + Není nastavena žádná Lightning adresa + Token zkopírován do schránky ŽIVĚ OFFLINE @@ -463,6 +467,8 @@ Nastavení Vždy Pouze Wi-Fi + Nikdy + Systém Světlý Tmavý @@ -483,4 +489,15 @@ Adresa Nostr byla ověřena Ověření adresy Nostr se nezdařilo Kontrola adresy Nostr + Vybrat/Zrušit vše + Výchozí + Vyberte relé pro pokračování + + Přeposílat Zapy na: + Podporující klienti budou přeposílat Zapy na níže uvedenou LN adresu nebo uživatelský profil místo vaší + + Zveřejnit polohu jako + Přidá Geohash vaší polohy do příspěvku. Veřejnost bude vědět, že se nacházíte do 5 km od aktuální polohy + + Přidat varování o citlivém obsahu před zobrazením vašeho obsahu. Toto je ideální pro obsah NSFW (nebezpečné pro práci) nebo obsah, který někteří lidé mohou považovat za urážlivý nebo znepokojující diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0b3d95b61..e8462631e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -47,6 +47,8 @@ " und " "im Kanal " Profilbanner + Zahlung erfolgreich + Fehler beim Parsen der Fehlermeldung " Folgen" " Anhänger" Profil @@ -421,7 +423,7 @@ anz der Bedingungen ist erforderlich Zapraiser bei %1$s. %2$s Sats bis zum Ziel Von Relay lesen In Relay schreiben - Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten + Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten Inhaber Version Software @@ -446,6 +448,8 @@ anz der Bedingungen ist erforderlich Zahlung Cashu-Token Einlösen + Keine Lightning-Adresse festgelegt + Token in die Zwischenablage kopiert LIVE OFFLINE @@ -472,6 +476,8 @@ anz der Bedingungen ist erforderlich Einstellungen Immer Nur WLAN + Nie + System Hell Dunkel @@ -492,4 +498,15 @@ anz der Bedingungen ist erforderlich Nostr-Adresse verifiziert Nostr-Adresse konnte nicht verifiziert werden Nostr-Adresse wird überprüft + Alle auswählen/abwählen + Standard + Wählen Sie ein Relais aus, um fortzufahren + + Weiterleiten von Zaps an: + Unterstützende Clients leiten Zaps an die LNAddress oder das Benutzerprofil unten weiter, anstatt an Ihre eigene Adresse + + Ort preisgeben als + Fügt dem Beitrag einen Geohash Ihres Standorts hinzu. Die Öffentlichkeit wird wissen, dass Sie sich innerhalb von 5 km (3 mi) vom aktuellen Standort befinden + + Fügt eine Warnung für sensiblen Inhalt hinzu, bevor Ihr Inhalt angezeigt wird. Dies ist ideal für NSFW-Inhalte (nicht sicher für die Arbeit) oder Inhalte, die manche Menschen als anstößig oder verstörend empfinden könnten \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 91c3e9368..9098a2aee 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -406,7 +406,6 @@ A ZapGyűjtés %1$s-nál. %2$s sats kell a célig Olvassás a csomópontból Írás a csomópontra - Hiba történt a csomópont információ szerzés közben a %1$s -tól Tulajdonos Verzió Szoftver diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 31bdc929b..aa1f3697a 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -411,7 +411,6 @@ Zapraiser at %1$s. %2$s sats to goal リレーから読み込み リレーに書き込み - %1$sからリレー情報を取得する際にエラーが発生しました 管理者 バージョン ソフトウェア diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3e501aa9a..55b995983 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -414,7 +414,6 @@ Zapraiser op %1$s. %2$s sats tot doel Lees van relay Schrijf naar relay - Er is een fout opgetreden bij het ophalen van de relay-informatie van %1$s Eigenaar Versie Software diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 78b311507..4c41da273 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -372,7 +372,6 @@ Arrecadação de Zaps em %1$s. %2$s sats para meta Ler do Relay Enviar para o Relay - Ocorreu um erro ao tentar obter informações do relay de %1$s Proprietário Versão Programa diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 1907a0559..16820baec 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -46,6 +46,8 @@ " och " "i kanalen " Profil Banner + Betalning lyckades + Fel vid tolkning av felmeddelande " Följer" " Följare" Profil @@ -107,7 +109,7 @@ Nya förfrågningar Blockerade användare Nya trådar - konversationer + Konversationer Anteckningar Svar "Följer" @@ -313,7 +315,7 @@ Fil Server - LnAdress or @Användare + LnAdress eller @Användare imgur.com - betrodd Imgur kan ändra filen @@ -386,7 +388,7 @@ Gå med i konversation Användare eller grupp ID - npub, nevent or hex + npub, nevent eller hex Skapa Gå med Idag @@ -409,7 +411,7 @@ Zapraiser på %1$s. %2$s sats kvar till målet Läs från Relay Skriv till Relay - Ett fel inträffade vid försök att hämta information från Relay %1$s + Ett fel inträffade vid försök att hämta information från Relay %1$s Ägare Version Programvara @@ -434,6 +436,8 @@ Betalning Cashu Token Inlösen + Ingen Lightning-adress angiven + Kopierade token till urklipp LIVE OFFLINE @@ -460,6 +464,8 @@ Inställningar Alltid Endast Wi-Fi + Aldrig + System Ljus Mörk @@ -480,6 +486,17 @@ Nostr-adress verifierad Verifikation av Nostr-adress misslyckades Kontrollerar Nostr-adress + Välj/Avmarkera alla + Standard + Välj ett relä för att fortsätta + + Vidarebefordra Zaps till: + Stödjande klienter kommer att vidarebefordra Zaps till LNAddress eller användarprofilen nedan istället för din egen + + Exponera plats som + Lägger till en Geohash av din plats i inlägget. Allmänheten kommer att veta att du befinner dig inom 5 km från nuvarande plats + + Lägger till en varning för känsligt innehåll innan ditt innehåll visas. Detta är idealiskt för NSFW-innehåll (inte säkert för arbete) eller innehåll som vissa personer kan uppleva som stötande eller störande diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 5dfd7b932..543f9ebf4 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -404,7 +404,6 @@ ஜாப்-திரட்டுதல் %1$s. இலக்கை அடைய %2$s படிப்பதற்கான ரிலே போஸ்ட் செய்வதற்கான ரிலே - %1$s ரிலே விவரங்கள் பெறுகையில் பிழை உரிமையாளர் பதிப்பு மென்பொருள் diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64d45e820..f5ae18cbe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,6 +48,8 @@ " and " "in channel " Profile Banner + Payment Successful + Error parsing error message " Following" " Followers" Profile @@ -433,7 +435,7 @@ Zapraiser at %1$s. %2$s sats to goal Read from Relay Write to Relay - An error ocurred trying to get relay information from %1$s + An error occurred trying to get relay information from %1$s Owner Version Software @@ -458,6 +460,8 @@ Payment Cashu Token Redeem + No Lightning Address set + Copied token to clipboard LIVE OFFLINE @@ -508,6 +512,7 @@ Checking Nostr address Select/Deselect all Default + Select a relay to continue Forward Zaps to: Supporting clients will forward zaps to the LNAddress or User Profile below instead of yours diff --git a/build.gradle b/build.gradle index a405f0fb0..b401c47bd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,15 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + buildscript { ext { - fragment_version = "1.6.0" + fragment_version = "1.6.1" lifecycle_version = '2.6.1' compose_ui_version = '1.4.3' nav_version = "2.6.0" room_version = "2.4.3" accompanist_version = '0.30.1' coil_version = '2.4.0' - vico_version = '1.7.1' + vico_version = '1.7.3' exoplayer_version = '1.1.0' } dependencies { @@ -15,13 +17,13 @@ buildscript { } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.0.2' apply false - id 'com.android.library' version '8.0.2' apply false - id 'org.jetbrains.kotlin.android' version '1.8.21' apply false - id 'org.jetbrains.kotlin.jvm' version '1.8.21' apply false + id 'com.android.application' version '8.1.0' apply false + id 'com.android.library' version '8.1.0' apply false + id 'org.jetbrains.kotlin.android' version '1.9.0' apply false + id 'org.jetbrains.kotlin.jvm' version '1.9.0' apply false } -task installGitHook(type: Copy) { +tasks.register('installGitHook', Copy) { from new File(rootProject.rootDir, 'git-hooks/pre-commit') from new File(rootProject.rootDir, 'git-hooks/pre-push') into { new File(rootProject.rootDir, '.git/hooks') } @@ -30,7 +32,7 @@ task installGitHook(type: Copy) { tasks.getByPath(':app:preBuild').dependsOn installGitHook subprojects { - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + tasks.withType(KotlinCompile).configureEach { kotlinOptions { if (project.findProperty("composeCompilerReports") == "true") { freeCompilerArgs += [