Merge branch 'vitorpamplona:main' into main

This commit is contained in:
Vel
2023-08-01 05:20:47 +05:30
committed by GitHub
35 changed files with 457 additions and 102 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.21" /> <option name="version" value="1.9.0" />
</component> </component>
</project> </project>
+8 -11
View File
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' 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' id 'com.google.gms.google-services'
} }
@@ -20,7 +20,7 @@ android {
vectorDrawables { vectorDrawables {
useSupportLibrary true 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 { buildTypes {
@@ -37,7 +37,7 @@ android {
} }
} }
flavorDimensions "channel" flavorDimensions = ["channel"]
productFlavors { productFlavors {
play { play {
@@ -73,7 +73,7 @@ android {
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion "1.4.7" kotlinCompilerExtensionVersion "1.5.1"
} }
packagingOptions { packagingOptions {
@@ -96,7 +96,7 @@ dependencies {
implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version"
// Navigation // Navigation
implementation("androidx.navigation:navigation-compose:$nav_version") implementation "androidx.navigation:navigation-compose:$nav_version"
// Observe Live data as State // Observe Live data as State
implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version" 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-runtime-compose:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$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" implementation "net.engawapg.lib:zoomable:1.4.3"
@@ -153,7 +153,7 @@ dependencies {
implementation "io.github.url-detector:url-detector:0.1.23" implementation "io.github.url-detector:url-detector:0.1.23"
// For QR generation // 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' implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
// Markdown // Markdown
@@ -196,16 +196,13 @@ dependencies {
// LibSodium for XChaCha encryption // LibSodium for XChaCha encryption
implementation "com.goterl:lazysodium-android:5.1.0@aar" 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 // Video compression lib
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1' implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
// Image compression lib // Image compression lib
implementation 'id.zelory:compressor:3.0.1' 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 'junit:junit:4.13.2'
testImplementation 'io.mockk:mockk:1.13.5' testImplementation 'io.mockk:mockk:1.13.5'
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha01' androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha01'
@@ -499,13 +499,46 @@ class GiftWrapEventTest {
val gossip = unwrapUnsealGossip(json, privateKey) val gossip = unwrapUnsealGossip(json, privateKey)
assertEquals("asdfasdfasdf", gossip?.content) assertEquals("asdfasdfasdf", gossip?.content)
assertEquals(0L, gossip?.createdAt) assertEquals(1690659269L, gossip?.createdAt)
assertEquals("827ba09d32ab81d62c60f657b350198c8aaba84372dab9ad3f4f6b8b7274b707", gossip?.id) assertEquals("827ba09d32ab81d62c60f657b350198c8aaba84372dab9ad3f4f6b8b7274b707", gossip?.id)
assertEquals(14, gossip?.kind) assertEquals(14, gossip?.kind)
assertEquals("subject", gossip?.tags?.firstOrNull()?.get(0)) assertEquals("subject", gossip?.tags?.firstOrNull()?.get(0))
assertEquals("test", gossip?.tags?.firstOrNull()?.get(1)) 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? { fun unwrapUnsealGossip(json: String, privateKey: HexKey): Gossip? {
val pkBytes = privateKey.hexToByteArray() val pkBytes = privateKey.hexToByteArray()
@@ -117,7 +117,7 @@ object ServiceManager {
LocalCache.pruneOldAndHiddenMessages(it) LocalCache.pruneOldAndHiddenMessages(it)
LocalCache.pruneHiddenMessages(it) LocalCache.pruneHiddenMessages(it)
LocalCache.pruneContactLists(it) LocalCache.pruneContactLists(it)
// LocalCache.pruneNonFollows(it) LocalCache.pruneRepliesAndReactions(it)
} }
} }
} }
@@ -1110,6 +1110,23 @@ object LocalCache {
refreshObservers(note) 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?) { private fun consume(event: SealedGossipEvent, relay: Relay?) {
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
@@ -1219,13 +1236,12 @@ object LocalCache {
} }
return notes.values.filter { return notes.values.filter {
(it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false) || it.event?.content()?.contains(text, true) ?: false ||
(it.event is PollNoteEvent && it.event?.content()?.contains(text, true) ?: false) || it.event?.matchTag1With(text) ?: false ||
(it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false) ||
it.idHex.startsWith(text, true) || it.idHex.startsWith(text, true) ||
it.idNote().startsWith(text, true) it.idNote().startsWith(text, true)
} + addressables.values.filter { } + 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.event?.matchTag1With(text) ?: false ||
it.idHex.startsWith(text, true) it.idHex.startsWith(text, true)
} }
@@ -1256,6 +1272,10 @@ object LocalCache {
it.value.clearLive() it.value.clearLive()
} }
addressables.forEach {
it.value.clearLive()
}
users.forEach { users.forEach {
it.value.clearLive() it.value.clearLive()
} }
@@ -1267,6 +1287,8 @@ object LocalCache {
channels.forEach { it -> channels.forEach { it ->
val toBeRemoved = it.value.pruneOldAndHiddenMessages(account) val toBeRemoved = it.value.pruneOldAndHiddenMessages(account)
val childrenToBeRemoved = mutableListOf<Note>()
toBeRemoved.forEach { toBeRemoved.forEach {
notes.remove(it.idHex) notes.remove(it.idHex)
// Doesn't need to clean up the replies and mentions.. Too small to matter. // Doesn't need to clean up the replies and mentions.. Too small to matter.
@@ -1275,17 +1297,84 @@ object LocalCache {
it.replyTo?.forEach { _ -> it.replyTo?.forEach { _ ->
it.removeReply(it) it.removeReply(it)
} }
childrenToBeRemoved.addAll(it.removeAllChildNotes())
} }
removeChildrenOf(childrenToBeRemoved)
if (toBeRemoved.size > 100 || it.value.notes.size > 100) { if (toBeRemoved.size > 100 || it.value.notes.size > 100) {
println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size} kept") 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<Note>()
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<Note>()
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) { fun pruneHiddenMessages(account: Account) {
checkNotInMainThread() checkNotInMainThread()
val childrenToBeRemoved = mutableListOf<Note>()
val toBeRemoved = account.hiddenUsers.map { userHex -> val toBeRemoved = account.hiddenUsers.map { userHex ->
( (
notes.values.filter { notes.values.filter {
@@ -1307,11 +1396,30 @@ object LocalCache {
} }
notes.remove(it.idHex) notes.remove(it.idHex)
childrenToBeRemoved.addAll(it.removeAllChildNotes())
} }
removeChildrenOf(childrenToBeRemoved)
println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden") println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden")
} }
fun removeChildrenOf(nextToBeRemoved: List<Note>) {
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) { fun pruneContactLists(userAccount: Account) {
checkNotInMainThread() checkNotInMainThread()
@@ -1359,6 +1467,7 @@ object LocalCache {
is ChannelMessageEvent -> consume(event, relay) is ChannelMessageEvent -> consume(event, relay)
is ChannelMetadataEvent -> consume(event) is ChannelMetadataEvent -> consume(event)
is ChannelMuteUserEvent -> consume(event) is ChannelMuteUserEvent -> consume(event)
is ChatMessageEvent -> consume(event, relay)
is ClassifiedsEvent -> consume(event) is ClassifiedsEvent -> consume(event)
is CommunityDefinitionEvent -> consume(event, relay) is CommunityDefinitionEvent -> consume(event, relay)
is CommunityPostApprovalEvent -> { is CommunityPostApprovalEvent -> {
@@ -158,6 +158,35 @@ open class Note(val idHex: String) {
boosts = boosts - note boosts = boosts - note
liveSet?.boosts?.invalidateData() liveSet?.boosts?.invalidateData()
} }
fun removeAllChildNotes(): Set<Note> {
val toBeRemoved = replies +
reactions.values.flatten() +
boosts +
reports.values.flatten() +
zaps.keys +
zaps.values.filterNotNull() +
zapPayments.keys +
zapPayments.values.filterNotNull()
replies = setOf<Note>()
reactions = mapOf<String, Set<Note>>()
boosts = setOf<Note>()
reports = mapOf<User, Set<Note>>()
zaps = mapOf<Note, Note?>()
zapPayments = mapOf<Note, Note?>()
relays = setOf<String>()
lastReactionsDownloadTime = emptyMap()
liveSet?.replies?.invalidateData()
liveSet?.reactions?.invalidateData()
liveSet?.boosts?.invalidateData()
liveSet?.reports?.invalidateData()
liveSet?.zaps?.invalidateData()
return toBeRemoved
}
fun removeReaction(note: Note) { fun removeReaction(note: Note) {
val tags = note.event?.tags() ?: emptyList() val tags = note.event?.tags() ?: emptyList()
val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+" val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"
@@ -554,6 +583,10 @@ open class Note(val idHex: String) {
zaps = emptyMap() zaps = emptyMap()
} }
fun clearEOSE() {
lastReactionsDownloadTime = emptyMap()
}
var liveSet: NoteLiveSet? = null var liveSet: NoteLiveSet? = null
fun live(): NoteLiveSet { fun live(): NoteLiveSet {
@@ -1,13 +1,17 @@
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
object TimeUtils { object TimeUtils {
const val fiveMinutes = 60 * 5 const val oneMinute = 60
const val oneHour = 60 * 60 const val fiveMinutes = 5 * oneMinute
const val oneDay = 24 * 60 * 60 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 now() = System.currentTimeMillis() / 1000
fun fiveMinutesAgo() = now() - fiveMinutes fun fiveMinutesAgo() = now() - fiveMinutes
fun oneHourAgo() = now() - oneHour fun oneHourAgo() = now() - oneHour
fun oneDayAgo() = now() - oneDay fun oneDayAgo() = now() - oneDay
fun eightHoursAgo() = now() - (oneHour * 8) fun eightHoursAgo() = now() - eightHours
fun oneWeekAgo() = now() - oneWeek
} }
@@ -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 { fun zappedAmount(): BigDecimal {
return zaps.mapNotNull { it.value?.event } return zaps.mapNotNull { it.value?.event }
.filterIsInstance<LnZapEvent>() .filterIsInstance<LnZapEvent>()
@@ -383,6 +393,22 @@ class Chatroom() {
roomMessages = roomMessages + msg roomMessages = roomMessages + msg
} }
} }
fun pruneMessagesToTheLatestOnly(): Set<Note> {
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 @Stable
@@ -6,7 +6,6 @@ import android.location.Geocoder
import android.location.Location import android.location.Location
import android.location.LocationListener import android.location.LocationListener
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle
import android.os.HandlerThread import android.os.HandlerThread
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -53,9 +52,6 @@ class LocationUtil(context: Context) {
locationStateFlow.value = location locationStateFlow.value = location
} }
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
}
override fun onProviderEnabled(provider: String) { override fun onProviderEnabled(provider: String) {
providerState.value = true providerState.value = true
} }
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User 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.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.JsonFilter
@@ -24,7 +25,7 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.PRIVATE_DMS), types = setOf(FeedType.PRIVATE_DMS),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(PrivateDmEvent.kind), kinds = listOf(PrivateDmEvent.kind, GiftWrapEvent.kind),
authors = listOf(myPeer.pubkeyHex), authors = listOf(myPeer.pubkeyHex),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)) tags = mapOf("p" to listOf(account.userProfile().pubkeyHex))
) )
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent 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.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.EOSEAccount
@@ -20,7 +21,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
fun createMessagesToMeFilter() = TypedFilter( fun createMessagesToMeFilter() = TypedFilter(
types = setOf(FeedType.PRIVATE_DMS), types = setOf(FeedType.PRIVATE_DMS),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(PrivateDmEvent.kind), kinds = listOf(PrivateDmEvent.kind, GiftWrapEvent.kind),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
) )
@@ -1,6 +1,10 @@
package com.vitorpamplona.amethyst.service 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.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.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
@@ -22,7 +26,7 @@ object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
hashToLoad 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 limit = 200
) )
) )
@@ -1,7 +1,11 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import androidx.compose.ui.text.capitalize 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.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.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
@@ -26,7 +30,7 @@ object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
hashToLoad.capitalize() 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 limit = 200
) )
) )
@@ -9,7 +9,7 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") { object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
private var searchString: String? = null private var searchString: String? = null
private fun createAnythingWithIDFilter(): List<TypedFilter>? { private fun createAnythingWithIDFilter(): List<TypedFilter>? {
@@ -54,6 +54,28 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.SEARCH), types = setOf(FeedType.SEARCH),
filter = JsonFilter( 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, search = mySearchString,
limit = 100 limit = 100
) )
@@ -12,7 +12,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
private var eventsToWatch = setOf<Note>() private var eventsToWatch = setOf<Note>()
private var addressesToWatch = setOf<Note>() private var addressesToWatch = setOf<Note>()
private fun createTagToAddressFilter(): List<TypedFilter>? { private fun createReactionsToWatchInAddressFilter(): List<TypedFilter>? {
val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch
if (addressesToWatch.isEmpty()) { if (addressesToWatch.isEmpty()) {
@@ -25,12 +25,15 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = JsonFilter( filter = JsonFilter(
kinds = listOf( kinds = listOf(
TextNoteEvent.kind, LongTextNoteEvent.kind, TextNoteEvent.kind,
ReactionEvent.kind, RepostEvent.kind, GenericRepostEvent.kind, ReportEvent.kind, ReactionEvent.kind,
LnZapEvent.kind, LnZapRequestEvent.kind, RepostEvent.kind,
BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind, GenericRepostEvent.kind,
PollNoteEvent.kind, AudioTrackEvent.kind, PinListEvent.kind, ReportEvent.kind,
PeopleListEvent.kind, BookmarkListEvent.kind LnZapEvent.kind,
PollNoteEvent.kind,
CommunityPostApprovalEvent.kind,
LiveActivitiesChatMessageEvent.kind
), ),
tags = mapOf("a" to listOf(aTag.toTag())), tags = mapOf("a" to listOf(aTag.toTag())),
since = it.lastReactionsDownloadTime, since = it.lastReactionsDownloadTime,
@@ -55,7 +58,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(aTag.kind), 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 { } else {
@@ -64,7 +68,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(aTag.kind), kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)), 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( filter = JsonFilter(
kinds = listOf( kinds = listOf(
TextNoteEvent.kind, TextNoteEvent.kind,
LongTextNoteEvent.kind,
ReactionEvent.kind, ReactionEvent.kind,
RepostEvent.kind, RepostEvent.kind,
GenericRepostEvent.kind, GenericRepostEvent.kind,
ReportEvent.kind, ReportEvent.kind,
LnZapEvent.kind, LnZapEvent.kind,
LnZapRequestEvent.kind, PollNoteEvent.kind
PollNoteEvent.kind,
HighlightEvent.kind,
AudioTrackEvent.kind,
PinListEvent.kind
), ),
tags = mapOf("e" to listOf(it.idHex)), tags = mapOf("e" to listOf(it.idHex)),
since = it.lastReactionsDownloadTime, since = it.lastReactionsDownloadTime,
@@ -154,7 +154,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
val reactions = createRepliesAndReactionsFilter() val reactions = createRepliesAndReactionsFilter()
val missing = createLoadEventsIfNotLoadedFilter() val missing = createLoadEventsIfNotLoadedFilter()
val addresses = createAddressFilter() val addresses = createAddressFilter()
val addressReactions = createTagToAddressFilter() val addressReactions = createReactionsToWatchInAddressFilter()
singleEventChannel.typedFilters = listOfNotNull(missing, addresses, reactions, addressReactions).flatten().ifEmpty { null } singleEventChannel.typedFilters = listOfNotNull(missing, addresses, reactions, addressReactions).flatten().ifEmpty { null }
} }
@@ -1,5 +1,7 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.toHexKey
class EventFactory { class EventFactory {
companion object { companion object {
fun create( fun create(
@@ -11,6 +13,34 @@ class EventFactory {
content: String, content: String,
sig: String, sig: String,
lenient: Boolean 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<List<String>>,
content: String,
sig: String,
lenient: Boolean
) = when (kind) { ) = when (kind) {
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig) AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.kind -> AppRecommendationEvent(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) ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(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) ClassifiedsEvent.kind -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
CommunityDefinitionEvent.kind -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig) CommunityDefinitionEvent.kind -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
@@ -19,13 +19,14 @@ class GiftWrapEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
private var innerEvent: Event? = null private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
fun cachedInnerEvent(privKey: ByteArray): Event? { fun cachedGossip(privKey: ByteArray): Event? {
if (innerEvent != null) return innerEvent val hex = privKey.toHexKey()
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
val myInnerEvent = unwrap(privKey = privKey) val myInnerEvent = unwrap(privKey = privKey)
innerEvent = myInnerEvent cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent)
return myInnerEvent return myInnerEvent
} }
@@ -2,12 +2,14 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.google.gson.annotations.SerializedName
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.CryptoUtils import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.service.EncryptedInfo import com.vitorpamplona.amethyst.service.EncryptedInfo
import com.vitorpamplona.amethyst.service.relays.Client
@Immutable @Immutable
class SealedGossipEvent( class SealedGossipEvent(
@@ -18,14 +20,16 @@ class SealedGossipEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
private var innerEvent: Gossip? = null private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
fun cachedGossip(privKey: ByteArray): Gossip? { fun cachedGossip(privKey: ByteArray): Event? {
if (innerEvent != null) return innerEvent val hex = privKey.toHexKey()
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
val myInnerEvent = unseal(privKey = privKey) val gossip = unseal(privKey = privKey)
innerEvent = myInnerEvent val event = gossip?.mergeWith(this)
return myInnerEvent cachedInnerEvent = cachedInnerEvent + Pair(hex, event)
return event
} }
fun unseal(privKey: ByteArray): Gossip? = try { fun unseal(privKey: ByteArray): Gossip? = try {
@@ -88,14 +92,28 @@ class SealedGossipEvent(
} }
} }
open class Gossip( class Gossip(
val id: HexKey, val id: HexKey?,
val pubKey: HexKey, @SerializedName("pubkey")
val createdAt: Long, val pubKey: HexKey?,
val kind: Int, @SerializedName("created_at")
val tags: List<List<String>>, val createdAt: Long?,
val content: String val kind: Int?,
val tags: List<List<String>>?,
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 { companion object {
fun create(event: Event): Gossip { fun create(event: Event): Gossip {
return Gossip(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content) return Gossip(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content)
@@ -18,6 +18,8 @@ import com.vitorpamplona.amethyst.ui.MainActivity
object NotificationUtils { object NotificationUtils {
private var dmChannel: NotificationChannel? = null private var dmChannel: NotificationChannel? = null
private var zapChannel: 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 { private fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel {
if (dmChannel != null) return dmChannel!! if (dmChannel != null) return dmChannel!!
@@ -70,7 +72,7 @@ object NotificationUtils {
val zapChannel = getOrCreateZapChannel(applicationContext) val zapChannel = getOrCreateZapChannel(applicationContext)
val channelId = applicationContext.getString(R.string.app_notification_zaps_channel_id) 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( fun NotificationManager.sendDMNotification(
@@ -84,7 +86,7 @@ object NotificationUtils {
val dmChannel = getOrCreateDMChannel(applicationContext) val dmChannel = getOrCreateDMChannel(applicationContext)
val channelId = applicationContext.getString(R.string.app_notification_dms_channel_id) 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( fun NotificationManager.sendNotification(
@@ -94,6 +96,7 @@ object NotificationUtils {
pictureUrl: String?, pictureUrl: String?,
uri: String, uri: String,
channelId: String, channelId: String,
notificationGroupKey: String,
applicationContext: Context applicationContext: Context
) { ) {
if (pictureUrl != null) { if (pictureUrl != null) {
@@ -110,6 +113,7 @@ object NotificationUtils {
picture = imageResult.drawable as? BitmapDrawable, picture = imageResult.drawable as? BitmapDrawable,
uri = uri, uri = uri,
channelId, channelId,
notificationGroupKey,
applicationContext = applicationContext applicationContext = applicationContext
) )
} else { } else {
@@ -120,6 +124,7 @@ object NotificationUtils {
picture = null, picture = null,
uri = uri, uri = uri,
channelId, channelId,
notificationGroupKey,
applicationContext = applicationContext applicationContext = applicationContext
) )
} }
@@ -132,6 +137,7 @@ object NotificationUtils {
picture: BitmapDrawable?, picture: BitmapDrawable?,
uri: String, uri: String,
channelId: String, channelId: String,
notificationGroupKey: String,
applicationContext: Context applicationContext: Context
) { ) {
val notId = id.hashCode() val notId = id.hashCode()
@@ -164,7 +170,8 @@ object NotificationUtils {
.setContentTitle(messageTitle) .setContentTitle(messageTitle)
.setContentText(applicationContext.getString(R.string.app_notification_private_message)) .setContentText(applicationContext.getString(R.string.app_notification_private_message))
.setLargeIcon(picture?.bitmap) .setLargeIcon(picture?.bitmap)
.setGroup(messageTitle) // .setGroup(messageTitle)
// .setGroup(notificationGroupKey) //-> Might need a Group summary as well before we activate this
.setContentIntent(contentPendingIntent) .setContentIntent(contentPendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true) .setAutoCancel(true)
@@ -178,7 +185,8 @@ object NotificationUtils {
.setContentTitle(messageTitle) .setContentTitle(messageTitle)
.setContentText(messageBody) .setContentText(messageBody)
.setLargeIcon(picture?.bitmap) .setLargeIcon(picture?.bitmap)
.setGroup(messageTitle) // .setGroup(messageTitle)
// .setGroup(notificationGroupKey) //-> Might need a Group summary as well before we activate this
.setContentIntent(contentPendingIntent) .setContentIntent(contentPendingIntent)
.setPublicVersion(builderPublic.build()) .setPublicVersion(builderPublic.build())
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
@@ -323,7 +323,7 @@ fun loadRelayInfo(
Toast Toast
.makeText( .makeText(
context, 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 Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -334,7 +334,7 @@ fun loadRelayInfo(
Toast Toast
.makeText( .makeText(
context, 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 Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -348,7 +348,7 @@ fun loadRelayInfo(
Toast Toast
.makeText( .makeText(
context, 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 Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -361,7 +361,7 @@ fun loadRelayInfo(
Toast Toast
.makeText( .makeText(
context, 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 Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -120,7 +120,7 @@ fun RelaySelectionDialog(
val selectedRelays = relays.filter { it.isSelected } val selectedRelays = relays.filter { it.isSelected }
if (selectedRelays.isEmpty()) { if (selectedRelays.isEmpty()) {
scope.launch { 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 return@PostButton
} }
@@ -149,7 +149,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
scope.launch { scope.launch {
Toast.makeText( Toast.makeText(
context, context,
"No Lightning Address set", context.getString(R.string.no_lightning_address_set),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -180,7 +180,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
var orignaltoken = token.token var orignaltoken = token.token
clipboardManager.setText(AnnotatedString("$orignaltoken")) clipboardManager.setText(AnnotatedString("$orignaltoken"))
scope.launch { 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()
} }
} }
}, },
@@ -25,17 +25,17 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
val notes = innerApplyFilter(LocalCache.notes.values) val notes = innerApplyFilter(LocalCache.notes.values, true)
val longFormNotes = innerApplyFilter(LocalCache.addressables.values) val longFormNotes = innerApplyFilter(LocalCache.addressables.values, false)
return sort(notes + longFormNotes) return sort(notes + longFormNotes)
} }
override fun applyFilter(collection: Set<Note>): Set<Note> { override fun applyFilter(collection: Set<Note>): Set<Note> {
return innerApplyFilter(collection) return innerApplyFilter(collection, false)
} }
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> { private fun innerApplyFilter(collection: Collection<Note>, ignoreAddressables: Boolean): Set<Note> {
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey() val isHiddenList = showHiddenKey()
@@ -52,6 +52,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
.filter { it -> .filter { it ->
val noteEvent = it.event 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) && (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)) && (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 // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
(isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) && (isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) &&
@@ -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", "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", "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", "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 ")
}
} }
@@ -1015,7 +1015,7 @@ fun DisplayLNAddress(
scope.launch { scope.launch {
Toast.makeText( Toast.makeText(
context, context,
"Payment Successful", // Turn this into a UI animation context.getString(R.string.payment_successful), // Turn this into a UI animation
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
@@ -1025,7 +1025,7 @@ fun DisplayLNAddress(
context, context,
response.error?.message response.error?.message
?: response.error?.code?.toString() ?: response.error?.code?.toString()
?: "Error parsing error message", ?: context.getString(R.string.error_parsing_error_message),
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
+18 -1
View File
@@ -47,6 +47,8 @@
<string name="and">" a "</string> <string name="and">" a "</string>
<string name="in_channel">"v kanálu "</string> <string name="in_channel">"v kanálu "</string>
<string name="profile_banner">Banner profilu</string> <string name="profile_banner">Banner profilu</string>
<string name="payment_successful">Platba úspěšná</string>
<string name="error_parsing_error_message">Chyba při zpracování chybové zprávy</string>
<string name="following">" Sleduje"</string> <string name="following">" Sleduje"</string>
<string name="followers">" Sledující"</string> <string name="followers">" Sledující"</string>
<string name="profile">Profil</string> <string name="profile">Profil</string>
@@ -412,7 +414,7 @@
<string name="sats_to_complete">Zapraiser na %1$s. Do cíle zbývá %2$s sats</string> <string name="sats_to_complete">Zapraiser na %1$s. Do cíle zbývá %2$s sats</string>
<string name="read_from_relay">Číst z Relay</string> <string name="read_from_relay">Číst z Relay</string>
<string name="write_to_relay">Zapisovat do Relay</string> <string name="write_to_relay">Zapisovat do Relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Při pokusu o získání informací z Relay se vyskytla chyba z %1$s</string> <string name="an_error_occurred_trying_to_get_relay_information">Při pokusu o získání informací z Relay se vyskytla chyba z %1$s</string>
<string name="owner">Vlastník</string> <string name="owner">Vlastník</string>
<string name="version">Verze</string> <string name="version">Verze</string>
<string name="software">Software</string> <string name="software">Software</string>
@@ -437,6 +439,8 @@
<string name="payment">Platba</string> <string name="payment">Platba</string>
<string name="cashu">Cashu Token</string> <string name="cashu">Cashu Token</string>
<string name="cashu_redeem">Vyměnit</string> <string name="cashu_redeem">Vyměnit</string>
<string name="no_lightning_address_set">Není nastavena žádná Lightning adresa</string>
<string name="copied_token_to_clipboard">Token zkopírován do schránky</string>
<string name="live_stream_live_tag">ŽIVĚ</string> <string name="live_stream_live_tag">ŽIVĚ</string>
<string name="live_stream_offline_tag">OFFLINE</string> <string name="live_stream_offline_tag">OFFLINE</string>
@@ -463,6 +467,8 @@
<string name="settings">Nastavení</string> <string name="settings">Nastavení</string>
<string name="connectivity_type_always">Vždy</string> <string name="connectivity_type_always">Vždy</string>
<string name="connectivity_type_wifi_only">Pouze Wi-Fi</string> <string name="connectivity_type_wifi_only">Pouze Wi-Fi</string>
<string name="connectivity_type_never">Nikdy</string>
<string name="system">Systém</string> <string name="system">Systém</string>
<string name="light">Světlý</string> <string name="light">Světlý</string>
<string name="dark">Tmavý</string> <string name="dark">Tmavý</string>
@@ -483,4 +489,15 @@
<string name="nip05_verified">Adresa Nostr byla ověřena</string> <string name="nip05_verified">Adresa Nostr byla ověřena</string>
<string name="nip05_failed">Ověření adresy Nostr se nezdařilo</string> <string name="nip05_failed">Ověření adresy Nostr se nezdařilo</string>
<string name="nip05_checking">Kontrola adresy Nostr</string> <string name="nip05_checking">Kontrola adresy Nostr</string>
<string name="select_deselect_all">Vybrat/Zrušit vše</string>
<string name="default_relays">Výchozí</string>
<string name="select_a_relay_to_continue">Vyberte relé pro pokračování</string>
<string name="zap_forward_title">Přeposílat Zapy na:</string>
<string name="zap_forward_explainer">Podporující klienti budou přeposílat Zapy na níže uvedenou LN adresu nebo uživatelský profil místo vaší</string>
<string name="geohash_title">Zveřejnit polohu jako </string>
<string name="geohash_explainer">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</string>
<string name="add_sensitive_content_explainer">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í</string>
</resources> </resources>
+18 -1
View File
@@ -47,6 +47,8 @@
<string name="and">" und "</string> <string name="and">" und "</string>
<string name="in_channel">"im Kanal "</string> <string name="in_channel">"im Kanal "</string>
<string name="profile_banner">Profilbanner</string> <string name="profile_banner">Profilbanner</string>
<string name="payment_successful">Zahlung erfolgreich</string>
<string name="error_parsing_error_message">Fehler beim Parsen der Fehlermeldung</string>
<string name="following">" Folgen"</string> <string name="following">" Folgen"</string>
<string name="followers">" Anhänger"</string> <string name="followers">" Anhänger"</string>
<string name="profile">Profil</string> <string name="profile">Profil</string>
@@ -421,7 +423,7 @@ anz der Bedingungen ist erforderlich</string>
<string name="sats_to_complete">Zapraiser bei %1$s. %2$s Sats bis zum Ziel</string> <string name="sats_to_complete">Zapraiser bei %1$s. %2$s Sats bis zum Ziel</string>
<string name="read_from_relay">Von Relay lesen</string> <string name="read_from_relay">Von Relay lesen</string>
<string name="write_to_relay">In Relay schreiben</string> <string name="write_to_relay">In Relay schreiben</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten</string> <string name="an_error_occurred_trying_to_get_relay_information">Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten</string>
<string name="owner">Inhaber</string> <string name="owner">Inhaber</string>
<string name="version">Version</string> <string name="version">Version</string>
<string name="software">Software</string> <string name="software">Software</string>
@@ -446,6 +448,8 @@ anz der Bedingungen ist erforderlich</string>
<string name="payment">Zahlung</string> <string name="payment">Zahlung</string>
<string name="cashu">Cashu-Token</string> <string name="cashu">Cashu-Token</string>
<string name="cashu_redeem">Einlösen</string> <string name="cashu_redeem">Einlösen</string>
<string name="no_lightning_address_set">Keine Lightning-Adresse festgelegt</string>
<string name="copied_token_to_clipboard">Token in die Zwischenablage kopiert</string>
<string name="live_stream_live_tag">LIVE</string> <string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string> <string name="live_stream_offline_tag">OFFLINE</string>
@@ -472,6 +476,8 @@ anz der Bedingungen ist erforderlich</string>
<string name="settings">Einstellungen</string> <string name="settings">Einstellungen</string>
<string name="connectivity_type_always">Immer</string> <string name="connectivity_type_always">Immer</string>
<string name="connectivity_type_wifi_only">Nur WLAN</string> <string name="connectivity_type_wifi_only">Nur WLAN</string>
<string name="connectivity_type_never">Nie</string>
<string name="system">System</string> <string name="system">System</string>
<string name="light">Hell</string> <string name="light">Hell</string>
<string name="dark">Dunkel</string> <string name="dark">Dunkel</string>
@@ -492,4 +498,15 @@ anz der Bedingungen ist erforderlich</string>
<string name="nip05_verified">Nostr-Adresse verifiziert</string> <string name="nip05_verified">Nostr-Adresse verifiziert</string>
<string name="nip05_failed">Nostr-Adresse konnte nicht verifiziert werden</string> <string name="nip05_failed">Nostr-Adresse konnte nicht verifiziert werden</string>
<string name="nip05_checking">Nostr-Adresse wird überprüft</string> <string name="nip05_checking">Nostr-Adresse wird überprüft</string>
<string name="select_deselect_all">Alle auswählen/abwählen</string>
<string name="default_relays">Standard</string>
<string name="select_a_relay_to_continue">Wählen Sie ein Relais aus, um fortzufahren</string>
<string name="zap_forward_title">Weiterleiten von Zaps an:</string>
<string name="zap_forward_explainer">Unterstützende Clients leiten Zaps an die LNAddress oder das Benutzerprofil unten weiter, anstatt an Ihre eigene Adresse</string>
<string name="geohash_title">Ort preisgeben als </string>
<string name="geohash_explainer">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</string>
<string name="add_sensitive_content_explainer">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</string>
</resources> </resources>
-1
View File
@@ -406,7 +406,6 @@
<string name="sats_to_complete">A ZapGyűjtés %1$s-nál. %2$s sats kell a célig</string> <string name="sats_to_complete">A ZapGyűjtés %1$s-nál. %2$s sats kell a célig</string>
<string name="read_from_relay">Olvassás a csomópontból</string> <string name="read_from_relay">Olvassás a csomópontból</string>
<string name="write_to_relay">Írás a csomópontra</string> <string name="write_to_relay">Írás a csomópontra</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Hiba történt a csomópont információ szerzés közben a %1$s -tól</string>
<string name="owner">Tulajdonos</string> <string name="owner">Tulajdonos</string>
<string name="version">Verzió</string> <string name="version">Verzió</string>
<string name="software">Szoftver</string> <string name="software">Szoftver</string>
-1
View File
@@ -411,7 +411,6 @@
<string name="sats_to_complete">Zapraiser at %1$s. %2$s sats to goal</string> <string name="sats_to_complete">Zapraiser at %1$s. %2$s sats to goal</string>
<string name="read_from_relay">リレーから読み込み</string> <string name="read_from_relay">リレーから読み込み</string>
<string name="write_to_relay">リレーに書き込み</string> <string name="write_to_relay">リレーに書き込み</string>
<string name="an_error_ocurred_trying_to_get_relay_information">%1$sからリレー情報を取得する際にエラーが発生しました</string>
<string name="owner">管理者</string> <string name="owner">管理者</string>
<string name="version">バージョン</string> <string name="version">バージョン</string>
<string name="software">ソフトウェア</string> <string name="software">ソフトウェア</string>
-1
View File
@@ -414,7 +414,6 @@
<string name="sats_to_complete">Zapraiser op %1$s. %2$s sats tot doel</string> <string name="sats_to_complete">Zapraiser op %1$s. %2$s sats tot doel</string>
<string name="read_from_relay">Lees van relay</string> <string name="read_from_relay">Lees van relay</string>
<string name="write_to_relay">Schrijf naar relay</string> <string name="write_to_relay">Schrijf naar relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Er is een fout opgetreden bij het ophalen van de relay-informatie van %1$s</string>
<string name="owner">Eigenaar</string> <string name="owner">Eigenaar</string>
<string name="version">Versie</string> <string name="version">Versie</string>
<string name="software">Software</string> <string name="software">Software</string>
@@ -372,7 +372,6 @@
<string name="sats_to_complete">Arrecadação de Zaps em %1$s. %2$s sats para meta</string> <string name="sats_to_complete">Arrecadação de Zaps em %1$s. %2$s sats para meta</string>
<string name="read_from_relay">Ler do Relay</string> <string name="read_from_relay">Ler do Relay</string>
<string name="write_to_relay">Enviar para o Relay</string> <string name="write_to_relay">Enviar para o Relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Ocorreu um erro ao tentar obter informações do relay de %1$s</string>
<string name="owner">Proprietário</string> <string name="owner">Proprietário</string>
<string name="version">Versão</string> <string name="version">Versão</string>
<string name="software">Programa</string> <string name="software">Programa</string>
+21 -4
View File
@@ -46,6 +46,8 @@
<string name="and">" och "</string> <string name="and">" och "</string>
<string name="in_channel">"i kanalen "</string> <string name="in_channel">"i kanalen "</string>
<string name="profile_banner">Profil Banner</string> <string name="profile_banner">Profil Banner</string>
<string name="payment_successful">Betalning lyckades</string>
<string name="error_parsing_error_message">Fel vid tolkning av felmeddelande</string>
<string name="following">" Följer"</string> <string name="following">" Följer"</string>
<string name="followers">" Följare"</string> <string name="followers">" Följare"</string>
<string name="profile">Profil</string> <string name="profile">Profil</string>
@@ -107,7 +109,7 @@
<string name="new_requests">Nya förfrågningar</string> <string name="new_requests">Nya förfrågningar</string>
<string name="blocked_users">Blockerade användare</string> <string name="blocked_users">Blockerade användare</string>
<string name="new_threads">Nya trådar</string> <string name="new_threads">Nya trådar</string>
<string name="conversations">konversationer</string> <string name="conversations">Konversationer</string>
<string name="notes">Anteckningar</string> <string name="notes">Anteckningar</string>
<string name="replies">Svar</string> <string name="replies">Svar</string>
<string name="follows">"Följer"</string> <string name="follows">"Följer"</string>
@@ -313,7 +315,7 @@
<string name="file_server">Fil Server</string> <string name="file_server">Fil Server</string>
<string name="zap_forward_lnAddress">LnAdress or @Användare</string> <string name="zap_forward_lnAddress">LnAdress eller @Användare</string>
<string name="upload_server_imgur">imgur.com - betrodd</string> <string name="upload_server_imgur">imgur.com - betrodd</string>
<string name="upload_server_imgur_explainer">Imgur kan ändra filen</string> <string name="upload_server_imgur_explainer">Imgur kan ändra filen</string>
@@ -386,7 +388,7 @@
<string name="channel_list_join_conversation">Gå med i konversation</string> <string name="channel_list_join_conversation">Gå med i konversation</string>
<string name="channel_list_user_or_group_id">Användare eller grupp ID</string> <string name="channel_list_user_or_group_id">Användare eller grupp ID</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent or hex</string> <string name="channel_list_user_or_group_id_demo">npub, nevent eller hex</string>
<string name="channel_list_create_channel">Skapa</string> <string name="channel_list_create_channel">Skapa</string>
<string name="channel_list_join_channel">Gå med</string> <string name="channel_list_join_channel">Gå med</string>
<string name="today">Idag</string> <string name="today">Idag</string>
@@ -409,7 +411,7 @@
<string name="sats_to_complete">Zapraiser på %1$s. %2$s sats kvar till målet</string> <string name="sats_to_complete">Zapraiser på %1$s. %2$s sats kvar till målet</string>
<string name="read_from_relay">Läs från Relay</string> <string name="read_from_relay">Läs från Relay</string>
<string name="write_to_relay">Skriv till Relay</string> <string name="write_to_relay">Skriv till Relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Ett fel inträffade vid försök att hämta information från Relay %1$s</string> <string name="an_error_occurred_trying_to_get_relay_information">Ett fel inträffade vid försök att hämta information från Relay %1$s</string>
<string name="owner">Ägare</string> <string name="owner">Ägare</string>
<string name="version">Version</string> <string name="version">Version</string>
<string name="software">Programvara</string> <string name="software">Programvara</string>
@@ -434,6 +436,8 @@
<string name="payment">Betalning</string> <string name="payment">Betalning</string>
<string name="cashu">Cashu Token</string> <string name="cashu">Cashu Token</string>
<string name="cashu_redeem">Inlösen</string> <string name="cashu_redeem">Inlösen</string>
<string name="no_lightning_address_set">Ingen Lightning-adress angiven</string>
<string name="copied_token_to_clipboard">Kopierade token till urklipp</string>
<string name="live_stream_live_tag">LIVE</string> <string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string> <string name="live_stream_offline_tag">OFFLINE</string>
@@ -460,6 +464,8 @@
<string name="settings">Inställningar</string> <string name="settings">Inställningar</string>
<string name="connectivity_type_always">Alltid</string> <string name="connectivity_type_always">Alltid</string>
<string name="connectivity_type_wifi_only">Endast Wi-Fi</string> <string name="connectivity_type_wifi_only">Endast Wi-Fi</string>
<string name="connectivity_type_never">Aldrig</string>
<string name="system">System</string> <string name="system">System</string>
<string name="light">Ljus</string> <string name="light">Ljus</string>
<string name="dark">Mörk</string> <string name="dark">Mörk</string>
@@ -480,6 +486,17 @@
<string name="nip05_verified">Nostr-adress verifierad</string> <string name="nip05_verified">Nostr-adress verifierad</string>
<string name="nip05_failed">Verifikation av Nostr-adress misslyckades</string> <string name="nip05_failed">Verifikation av Nostr-adress misslyckades</string>
<string name="nip05_checking">Kontrollerar Nostr-adress</string> <string name="nip05_checking">Kontrollerar Nostr-adress</string>
<string name="select_deselect_all">Välj/Avmarkera alla</string>
<string name="default_relays">Standard</string>
<string name="select_a_relay_to_continue">Välj ett relä för att fortsätta</string>
<string name="zap_forward_title">Vidarebefordra Zaps till:</string>
<string name="zap_forward_explainer">Stödjande klienter kommer att vidarebefordra Zaps till LNAddress eller användarprofilen nedan istället för din egen</string>
<string name="geohash_title">Exponera plats som </string>
<string name="geohash_explainer">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</string>
<string name="add_sensitive_content_explainer">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</string>
</resources> </resources>
-1
View File
@@ -404,7 +404,6 @@
<string name="sats_to_complete">ஜாப்-திரட்டுதல் %1$s. இலக்கை அடைய %2$s</string> <string name="sats_to_complete">ஜாப்-திரட்டுதல் %1$s. இலக்கை அடைய %2$s</string>
<string name="read_from_relay">படிப்பதற்கான ரிலே</string> <string name="read_from_relay">படிப்பதற்கான ரிலே</string>
<string name="write_to_relay">போஸ்ட் செய்வதற்கான ரிலே</string> <string name="write_to_relay">போஸ்ட் செய்வதற்கான ரிலே</string>
<string name="an_error_ocurred_trying_to_get_relay_information">%1$s ரிலே விவரங்கள் பெறுகையில் பிழை</string>
<string name="owner">உரிமையாளர்</string> <string name="owner">உரிமையாளர்</string>
<string name="version">பதிப்பு</string> <string name="version">பதிப்பு</string>
<string name="software">மென்பொருள்</string> <string name="software">மென்பொருள்</string>
+6 -1
View File
@@ -48,6 +48,8 @@
<string name="and">" and "</string> <string name="and">" and "</string>
<string name="in_channel">"in channel "</string> <string name="in_channel">"in channel "</string>
<string name="profile_banner">Profile Banner</string> <string name="profile_banner">Profile Banner</string>
<string name="payment_successful">Payment Successful</string>
<string name="error_parsing_error_message">Error parsing error message</string>
<string name="following">" Following"</string> <string name="following">" Following"</string>
<string name="followers">" Followers"</string> <string name="followers">" Followers"</string>
<string name="profile">Profile</string> <string name="profile">Profile</string>
@@ -433,7 +435,7 @@
<string name="sats_to_complete">Zapraiser at %1$s. %2$s sats to goal</string> <string name="sats_to_complete">Zapraiser at %1$s. %2$s sats to goal</string>
<string name="read_from_relay">Read from Relay</string> <string name="read_from_relay">Read from Relay</string>
<string name="write_to_relay">Write to Relay</string> <string name="write_to_relay">Write to Relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">An error ocurred trying to get relay information from %1$s</string> <string name="an_error_occurred_trying_to_get_relay_information">An error occurred trying to get relay information from %1$s</string>
<string name="owner">Owner</string> <string name="owner">Owner</string>
<string name="version">Version</string> <string name="version">Version</string>
<string name="software">Software</string> <string name="software">Software</string>
@@ -458,6 +460,8 @@
<string name="payment">Payment</string> <string name="payment">Payment</string>
<string name="cashu">Cashu Token</string> <string name="cashu">Cashu Token</string>
<string name="cashu_redeem">Redeem</string> <string name="cashu_redeem">Redeem</string>
<string name="no_lightning_address_set">No Lightning Address set</string>
<string name="copied_token_to_clipboard">Copied token to clipboard</string>
<string name="live_stream_live_tag">LIVE</string> <string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string> <string name="live_stream_offline_tag">OFFLINE</string>
@@ -508,6 +512,7 @@
<string name="nip05_checking">Checking Nostr address</string> <string name="nip05_checking">Checking Nostr address</string>
<string name="select_deselect_all">Select/Deselect all</string> <string name="select_deselect_all">Select/Deselect all</string>
<string name="default_relays">Default</string> <string name="default_relays">Default</string>
<string name="select_a_relay_to_continue">Select a relay to continue</string>
<string name="zap_forward_title">Forward Zaps to:</string> <string name="zap_forward_title">Forward Zaps to:</string>
<string name="zap_forward_explainer">Supporting clients will forward zaps to the LNAddress or User Profile below instead of yours</string> <string name="zap_forward_explainer">Supporting clients will forward zaps to the LNAddress or User Profile below instead of yours</string>
+10 -8
View File
@@ -1,13 +1,15 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript { buildscript {
ext { ext {
fragment_version = "1.6.0" fragment_version = "1.6.1"
lifecycle_version = '2.6.1' lifecycle_version = '2.6.1'
compose_ui_version = '1.4.3' compose_ui_version = '1.4.3'
nav_version = "2.6.0" nav_version = "2.6.0"
room_version = "2.4.3" room_version = "2.4.3"
accompanist_version = '0.30.1' accompanist_version = '0.30.1'
coil_version = '2.4.0' coil_version = '2.4.0'
vico_version = '1.7.1' vico_version = '1.7.3'
exoplayer_version = '1.1.0' exoplayer_version = '1.1.0'
} }
dependencies { dependencies {
@@ -15,13 +17,13 @@ buildscript {
} }
}// Top-level build file where you can add configuration options common to all sub-projects/modules. }// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '8.0.2' apply false id 'com.android.application' version '8.1.0' apply false
id 'com.android.library' version '8.0.2' apply false id 'com.android.library' version '8.1.0' apply false
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
id 'org.jetbrains.kotlin.jvm' version '1.8.21' 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-commit')
from new File(rootProject.rootDir, 'git-hooks/pre-push') from new File(rootProject.rootDir, 'git-hooks/pre-push')
into { new File(rootProject.rootDir, '.git/hooks') } into { new File(rootProject.rootDir, '.git/hooks') }
@@ -30,7 +32,7 @@ task installGitHook(type: Copy) {
tasks.getByPath(':app:preBuild').dependsOn installGitHook tasks.getByPath(':app:preBuild').dependsOn installGitHook
subprojects { subprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { tasks.withType(KotlinCompile).configureEach {
kotlinOptions { kotlinOptions {
if (project.findProperty("composeCompilerReports") == "true") { if (project.findProperty("composeCompilerReports") == "true") {
freeCompilerArgs += [ freeCompilerArgs += [