diff --git a/.idea/misc.xml b/.idea/misc.xml index 4d86ddb47..bdd92780c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index ba48d5250..25972e18e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -159,9 +159,10 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' testImplementation 'junit:junit:4.13.2' + testImplementation "io.mockk:mockk:1.13.4" androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index a199789ae..0301eda33 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -415,7 +415,7 @@ class Account( event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray()) } else { - event?.content + event?.content() } } @@ -590,4 +590,4 @@ class AccountLiveData(private val account: Account): LiveData(Acco } } -class AccountState(val account: Account) \ No newline at end of file +class AccountState(val account: Account) 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 fcc1a8f46..d969916f5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -192,7 +192,7 @@ object LocalCache { note.loadEvent(event, author, mentions, replyTo) - //Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content?.take(100)} ${formattedDateTime(event.createdAt)}") + //Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.take(100)} ${formattedDateTime(event.createdAt)}") // Prepares user's profile view. author.addNote(note) @@ -223,7 +223,7 @@ object LocalCache { } // Already processed this event. - if (note.event?.id == event.id) return + if (note.event?.id() == event.id) return if (antiSpam.isSpam(event)) { relay?.let { @@ -594,7 +594,7 @@ object LocalCache { note.loadEvent(event, author, mentions, replyTo) - //Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content} ${formattedDateTime(event.createdAt)}") + //Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()} ${formattedDateTime(event.createdAt)}") // Adds notifications to users. mentions.forEach { @@ -700,8 +700,8 @@ object LocalCache { fun findNotesStartingWith(text: String): List { return notes.values.filter { - (it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false) - || (it.event is ChannelMessageEvent && it.event?.content?.contains(text, true) ?: false) + (it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false) + || (it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false) || it.idHex.startsWith(text, true) || it.idNote().startsWith(text, true) } + addressables.values.filter { @@ -770,7 +770,7 @@ object LocalCache { val toBeRemoved = notes .filter { - (it.value.author == null || it.value.author!! !in followSet) && it.value.event?.kind == TextNoteEvent.kind && it.value.liveSet?.isInUse() != true + (it.value.author == null || it.value.author!! !in followSet) && it.value.event?.kind() == TextNoteEvent.kind && it.value.liveSet?.isInUse() != true } toBeRemoved.forEach { @@ -862,4 +862,4 @@ class LocalCacheLiveData(val cache: LocalCache): LiveData(Local class LocalCacheState(val cache: LocalCache) { -} \ No newline at end of file +} 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 f8bfa09a2..2544afd17 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -2,14 +2,7 @@ package com.vitorpamplona.amethyst.model import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource -import com.vitorpamplona.amethyst.service.model.ATag -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.LnZapEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent +import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.note.toShortenHex import fr.acinq.secp256k1.Hex @@ -27,7 +20,6 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import com.vitorpamplona.amethyst.service.model.Event val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") @@ -36,13 +28,13 @@ class AddressableNote(val address: ATag): Note(address.toNAddr()) { override fun idNote() = address.toNAddr() override fun idDisplayNote() = idNote().toShortenHex() override fun address() = address - override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt + override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt() } open class Note(val idHex: String) { // These fields are only available after the Text Note event is received. // They are immutable after that. - var event: Event? = null + var event: EventInterface? = null var author: User? = null var mentions: List? = null var replyTo: List? = null @@ -79,7 +71,7 @@ open class Note(val idHex: String) { open fun address() = (event as? LongTextNoteEvent)?.address() - open fun createdAt() = event?.createdAt + open fun createdAt() = event?.createdAt() fun loadEvent(event: Event, author: User, mentions: List, replyTo: List) { this.event = event @@ -256,11 +248,11 @@ open class Note(val idHex: String) { } fun directlyCiteUsersHex(): Set { - val matcher = tagSearch.matcher(event?.content ?: "") + val matcher = tagSearch.matcher(event?.content() ?: "") val returningList = mutableSetOf() while (matcher.find()) { try { - val tag = matcher.group(1)?.let { event?.tags?.get(it.toInt()) } + val tag = matcher.group(1)?.let { event?.tags()?.get(it.toInt()) } if (tag != null && tag[0] == "p") { returningList.add(tag[1]) } @@ -272,11 +264,11 @@ open class Note(val idHex: String) { } fun directlyCiteUsers(): Set { - val matcher = tagSearch.matcher(event?.content ?: "") + val matcher = tagSearch.matcher(event?.content() ?: "") val returningList = mutableSetOf() while (matcher.find()) { try { - val tag = matcher.group(1)?.let { event?.tags?.get(it.toInt()) } + val tag = matcher.group(1)?.let { event?.tags()?.get(it.toInt()) } if (tag != null && tag[0] == "p") { LocalCache.checkGetOrCreateUser(tag[1])?.let { returningList.add(it) @@ -309,7 +301,7 @@ open class Note(val idHex: String) { } fun reactedBy(loggedIn: User, content: String): List { - return reactions.filter { it.author == loggedIn && it.event?.content == content } + return reactions.filter { it.author == loggedIn && it.event?.content() == content } } fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index be2366c66..46f905070 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -11,7 +11,7 @@ class ThreadAssembler { testedNotes.add(note) - val markedAsRoot = note.event?.tags?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) + val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot) val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } @@ -73,4 +73,4 @@ class ThreadAssembler { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt index 49471bf4a..02b9555f6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt @@ -54,7 +54,7 @@ object UrlCachedPreviewer { } fun preloadPreviewsFor(note: Note) { - note.event?.content?.let { + note.event?.content()?.let { findUrlsInMessage(it).forEach { val removedParamsFromUrl = it.split("?")[0].lowercase() if (imageExtension.matcher(removedParamsFromUrl).matches()) { 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 b42f5b48c..737dcd5b8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -61,6 +61,8 @@ class User(val pubkeyHex: String) { fun pubkeyNpub() = pubkey().toNpub() fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex() + override fun toString(): String = pubkeyHex + fun toBestDisplayName(): String { return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 772ca170c..f513d2f59 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -1,25 +1,16 @@ package com.vitorpamplona.amethyst.service.model -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.JsonArray -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.google.gson.JsonObject -import com.google.gson.JsonPrimitive -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer +import com.google.gson.* import com.google.gson.annotations.SerializedName import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Secp256k1 -import java.lang.reflect.Type -import java.security.MessageDigest -import java.util.Date import nostr.postr.Utils import nostr.postr.toHex +import java.lang.reflect.Type +import java.security.MessageDigest +import java.util.* open class Event( val id: HexKey, @@ -29,34 +20,27 @@ open class Event( val tags: List>, val content: String, val sig: HexKey -) { - fun toJson(): String = gson.toJson(this) +): EventInterface { + override fun id(): HexKey = id - fun generateId(): String { - val rawEvent = listOf( - 0, - pubKey, - createdAt, - kind, - tags, - content - ) + override fun pubKey(): HexKey = pubKey - // GSON decided to hardcode these replacements. - // They break Nostr's hash check. - // These lines revert their code. - // https://github.com/google/gson/issues/2295 - val rawEventJson = gson.toJson(rawEvent) - .replace("\\u2028", "\u2028") - .replace("\\u2029", "\u2029") + override fun createdAt(): Long = createdAt - return sha256.digest(rawEventJson.toByteArray()).toHexKey() - } + override fun kind(): Int = kind + + override fun tags(): List> = tags + + override fun content(): String = content + + override fun sig(): HexKey = sig + + override fun toJson(): String = gson.toJson(this) /** * Checks if the ID is correct and then if the pubKey's secret key signed the event. */ - fun checkSignature() { + override fun checkSignature() { if (!id.contentEquals(generateId())) { throw Exception( """|Unexpected ID. @@ -70,18 +54,29 @@ open class Event( } } - fun hasValidSignature(): Boolean { + override fun hasValidSignature(): Boolean { if (!id.contentEquals(generateId())) { return false } - if (!Secp256k1.get().verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) { - return false - } - return true + return secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey)) } - class EventDeserializer : JsonDeserializer { + private fun generateId(): String { + val rawEvent = listOf(0, pubKey, createdAt, kind, tags, content) + + // GSON decided to hardcode these replacements. + // They break Nostr's hash check. + // These lines revert their code. + // https://github.com/google/gson/issues/2295 + val rawEventJson = gson.toJson(rawEvent) + .replace("\\u2028", "\u2028") + .replace("\\u2029", "\u2029") + + return sha256.digest(rawEventJson.toByteArray()).toHexKey() + } + + private class EventDeserializer : JsonDeserializer { override fun deserialize( json: JsonElement, typeOfT: Type?, @@ -102,7 +97,7 @@ open class Event( } } - class EventSerializer : JsonSerializer { + private class EventSerializer : JsonSerializer { override fun serialize( src: Event, typeOfSrc: Type?, @@ -128,7 +123,7 @@ open class Event( } } - class ByteArrayDeserializer : JsonDeserializer { + private class ByteArrayDeserializer : JsonDeserializer { override fun deserialize( json: JsonElement, typeOfT: Type?, @@ -136,7 +131,7 @@ open class Event( ): ByteArray = Hex.decode(json.asString) } - class ByteArraySerializer : JsonSerializer { + private class ByteArraySerializer : JsonSerializer { override fun serialize( src: ByteArray, typeOfSrc: Type?, @@ -202,4 +197,4 @@ open class Event( return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt new file mode 100644 index 000000000..91dfbf96e --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt @@ -0,0 +1,25 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey + +interface EventInterface { + fun id(): HexKey + + fun pubKey(): HexKey + + fun createdAt(): Long + + fun kind(): Int + + fun tags(): List> + + fun content(): String + + fun sig(): HexKey + + fun toJson(): String + + fun checkSignature() + + fun hasValidSignature(): Boolean +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index e17c6e672..4f6fa852c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -21,12 +21,12 @@ class LnZapRequestEvent ( companion object { const val kind = 9734 - fun create(originalNote: Event, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { + fun create(originalNote: EventInterface, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { val content = "" val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( - listOf("e", originalNote.id), - listOf("p", originalNote.pubKey), + listOf("e", originalNote.id()), + listOf("p", originalNote.pubKey()), listOf("relays") + relays ) if (originalNote is LongTextNoteEvent) { @@ -84,4 +84,4 @@ class LnZapRequestEvent ( ] ] } -*/ \ No newline at end of file +*/ diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index c01972758..4676bee1e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -22,18 +22,18 @@ class ReactionEvent ( companion object { const val kind = 7 - fun createWarning(originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { + fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { return create("\u26A0\uFE0F", originalNote, privateKey, createdAt) } - fun createLike(originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { + fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { return create("+", originalNote, privateKey, createdAt) } - fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { + fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - var tags = listOf( listOf("e", originalNote.id), listOf("p", originalNote.pubKey)) + var tags = listOf( listOf("e", originalNote.id()), listOf("p", originalNote.pubKey())) if (originalNote is LongTextNoteEvent) { tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) } @@ -43,4 +43,4 @@ class ReactionEvent ( return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index 6e10f5b5e..765f98abd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -53,11 +53,11 @@ class ReportEvent ( companion object { const val kind = 1984 - fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { + fun create(reportedPost: EventInterface, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { val content = "" - val reportPostTag = listOf("e", reportedPost.id, type.name.lowercase()) - val reportAuthorTag = listOf("p", reportedPost.pubKey, type.name.lowercase()) + val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase()) + val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase()) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags:List> = listOf(reportPostTag, reportAuthorTag) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index 5a9513c21..5e7b5f4b5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -29,14 +29,14 @@ class RepostEvent ( companion object { const val kind = 6 - fun create(boostedPost: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent { + fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent { val content = boostedPost.toJson() - val replyToPost = listOf("e", boostedPost.id) - val replyToAuthor = listOf("p", boostedPost.pubKey) + val replyToPost = listOf("e", boostedPost.id()) + val replyToAuthor = listOf("p", boostedPost.pubKey()) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - var tags:List> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor)) + var tags:List> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor)) if (boostedPost is LongTextNoteEvent) { tags = tags + listOf( listOf("a", boostedPost.address().toTag()) ) @@ -47,4 +47,4 @@ class RepostEvent ( return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/zaps/UserZaps.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/zaps/UserZaps.kt new file mode 100644 index 000000000..08e8fe865 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/zaps/UserZaps.kt @@ -0,0 +1,16 @@ +package com.vitorpamplona.amethyst.service.model.zaps + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.LnZapEvent + +object UserZaps { + fun groupByUser(zaps: Map?): List> { + if (zaps == null) return emptyList() + + return (zaps + .filter { it.value != null } + .toList() + .sortedBy { (it.second?.event as? LnZapEvent)?.amount } + .reversed()) as List> + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 776a670fb..dff28ef7e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.EventInterface /** * The Nostr Client manages multiple personae the user may switch between. Events are received and @@ -62,7 +63,7 @@ object Client: RelayPool.Listener { RelayPool.sendFilterOnlyIfDisconnected() } - fun send(signedEvent: Event) { + fun send(signedEvent: EventInterface) { RelayPool.send(signedEvent) } @@ -146,4 +147,4 @@ object Client: RelayPool.Listener { */ open fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) = Unit } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 0804d07d7..21cd7b0ef 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -4,6 +4,7 @@ import android.util.Log import com.google.gson.JsonElement import java.util.Date import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.EventInterface import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -193,7 +194,7 @@ class Relay( } } - fun send(signedEvent: Event) { + fun send(signedEvent: EventInterface) { if (write) { socket?.send("""["EVENT",${signedEvent.toJson()}]""") eventUploadCounter++ diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index bb9952a82..99bddac0d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.EventInterface /** * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. @@ -54,7 +55,7 @@ object RelayPool: Relay.Listener { relays.forEach { it.sendFilterOnlyIfDisconnected() } } - fun send(signedEvent: Event) { + fun send(signedEvent: EventInterface) { relays.forEach { it.send(signedEvent) } } @@ -128,4 +129,4 @@ class RelayPoolLiveData(val relays: RelayPool): LiveData(RelayPo } } -class RelayPoolState(val relays: RelayPool) \ No newline at end of file +class RelayPoolState(val relays: RelayPool) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt index 3531eef35..0cb2532de 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt @@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.model.LnZapEvent +import com.vitorpamplona.amethyst.service.model.zaps.UserZaps object UserProfileZapsFeedFilter: FeedFilter>() { var user: User? = null @@ -13,10 +13,6 @@ object UserProfileZapsFeedFilter: FeedFilter>() { } override fun feed(): List> { - return (user?.zaps - ?.filter { it.value != null } - ?.toList() - ?.sortedBy { (it.second?.event as? LnZapEvent)?.amount } - ?.reversed() ?: emptyList()) as List> + return UserZaps.groupByUser(user?.zaps) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index 4a1727088..57d747086 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -77,7 +77,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr } else if (noteEvent is ChannelMetadataEvent) { "${stringResource(R.string.channel_information_changed_to)} " } else { - noteEvent?.content + noteEvent?.content() } channel?.let { channel -> var hasNewMessages by remember { mutableStateOf(false) } @@ -127,7 +127,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr LaunchedEffect(key1 = notificationCache, key2 = note) { noteEvent?.let { - hasNewMessages = it.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) + hasNewMessages = it.createdAt() > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) } } @@ -263,4 +263,4 @@ fun NewItemsBubble() { .align(Alignment.Center) ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 87f749abc..bce2500e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -265,7 +265,7 @@ fun ChatroomMessageCompose( eventContent, canPreview, Modifier, - note.event?.tags, + note.event?.tags(), backgroundBubbleColor, accountViewModel, navController @@ -275,7 +275,7 @@ fun ChatroomMessageCompose( stringResource(R.string.could_not_decrypt_the_message), true, Modifier, - note.event?.tags, + note.event?.tags(), backgroundBubbleColor, accountViewModel, navController diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index d55a57208..eaedbac13 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -370,7 +370,7 @@ fun NoteCompose( eventContent, canPreview = canPreview && !makeItShort, Modifier.fillMaxWidth(), - noteEvent.tags, + noteEvent.tags(), backgroundColor, accountViewModel, navController @@ -724,4 +724,4 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index f056afb5b..dd9f9f4a8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -119,7 +119,7 @@ private fun FeedLoaded( route = "Room/${userToComposeOn.pubkeyHex}" } - notificationCache.cache.markAsRead(route, it.createdAt, context) + notificationCache.cache.markAsRead(route, it.createdAt(), context) } } markAsRead.value = false diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index e0c7b07b2..19773ddc6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -307,7 +307,7 @@ fun NoteMaster(baseNote: Note, Row(modifier = Modifier.padding(horizontal = 12.dp)) { Column() { - val eventContent = note.event?.content + val eventContent = note.event?.content() val canPreview = note.author == account.userProfile() || (note.author?.let { account.userProfile().isFollowing(it) } ?: true ) @@ -318,7 +318,7 @@ fun NoteMaster(baseNote: Note, eventContent, canPreview, Modifier.fillMaxWidth(), - note.event?.tags, + note.event?.tags(), MaterialTheme.colors.background, accountViewModel, navController diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserZapsTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserZapsTest.kt new file mode 100644 index 000000000..07bf3048b --- /dev/null +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserZapsTest.kt @@ -0,0 +1,56 @@ +package com.vitorpamplona.amethyst.service.zaps + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.EventInterface +import com.vitorpamplona.amethyst.service.model.zaps.UserZaps +import io.mockk.* +import org.junit.Assert +import org.junit.Test + +class UserZapsTest { + @Test + fun nothing() { + Assert.assertEquals(1, 1) + } + + @Test + fun user_without_zaps() { + val actual = UserZaps.groupByUser(zaps = null) + + Assert.assertEquals(emptyList>(), actual) + } + + @Test + fun group_by_user_with_just_one_user() { + val u1 = mockk() + val z1 = mockk() + val z2 = mockk() + val zaps: Map = mapOf(u1 to z1, u1 to z2) + val actual = UserZaps.groupByUser(zaps) + + Assert.assertEquals(listOf(Pair(u1, z2)), actual) + } + + @Test + fun group_by_user() { + // FIXME: not working yet... +// IDEA: +// [ (u1 -> z1) (u1 -> z2) (u2 -> z3) ] +// [ (u1 -> z1 + z2) (u2 -> z3)] + val u1 = mockk() + val u2 = mockk() + + val z1 = mockk() + val z2 = mockk() + val z3 = mockk() + every { z3.event } returns mockk() + + val zaps: Map = mapOf(u1 to z1, u1 to z2, u2 to z3) + val actual = UserZaps.groupByUser(zaps) + + Assert.assertEquals( + listOf(Pair(u1, z1), Pair(u1, z2), Pair(u2, z3)), + actual + ) + } +}