Merge pull request #205 from Chemaclass/refactor-UserProfileZapsFeedFilter

Refactor UserProfileZapsFeedFilter
This commit is contained in:
Vitor Pamplona
2023-03-06 12:33:48 -05:00
committed by GitHub
27 changed files with 243 additions and 123 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">
+1
View File
@@ -159,6 +159,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation "io.mockk:mockk:1.13.4"
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
@@ -415,7 +415,7 @@ class Account(
event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray()) event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray())
} else { } else {
event?.content event?.content()
} }
} }
@@ -194,7 +194,7 @@ object LocalCache {
note.loadEvent(event, author, mentions, replyTo) 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. // Prepares user's profile view.
author.addNote(note) author.addNote(note)
@@ -225,7 +225,7 @@ object LocalCache {
} }
// Already processed this event. // Already processed this event.
if (note.event?.id == event.id) return if (note.event?.id() == event.id) return
if (antiSpam.isSpam(event)) { if (antiSpam.isSpam(event)) {
relay?.let { relay?.let {
@@ -662,7 +662,7 @@ object LocalCache {
note.loadEvent(event, author, mentions, replyTo) 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. // Adds notifications to users.
mentions.forEach { mentions.forEach {
@@ -768,8 +768,8 @@ object LocalCache {
fun findNotesStartingWith(text: String): List<Note> { fun findNotesStartingWith(text: String): List<Note> {
return notes.values.filter { return notes.values.filter {
(it.event is TextNoteEvent && 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.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 {
@@ -838,7 +838,7 @@ object LocalCache {
val toBeRemoved = notes val toBeRemoved = notes
.filter { .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 { toBeRemoved.forEach {
@@ -2,14 +2,7 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.model.ATag import com.vitorpamplona.amethyst.service.model.*
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.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
@@ -27,7 +20,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import com.vitorpamplona.amethyst.service.model.Event
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") 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 idNote() = address.toNAddr()
override fun idDisplayNote() = idNote().toShortenHex() override fun idDisplayNote() = idNote().toShortenHex()
override fun address() = address 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) { open class Note(val idHex: String) {
// These fields are only available after the Text Note event is received. // These fields are only available after the Text Note event is received.
// They are immutable after that. // They are immutable after that.
var event: Event? = null var event: EventInterface? = null
var author: User? = null var author: User? = null
var mentions: List<User>? = null var mentions: List<User>? = null
var replyTo: List<Note>? = null var replyTo: List<Note>? = null
@@ -79,7 +71,7 @@ open class Note(val idHex: String) {
open fun address() = (event as? LongTextNoteEvent)?.address() 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<User>, replyTo: List<Note>) { fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: List<Note>) {
this.event = event this.event = event
@@ -256,11 +248,11 @@ open class Note(val idHex: String) {
} }
fun directlyCiteUsersHex(): Set<HexKey> { fun directlyCiteUsersHex(): Set<HexKey> {
val matcher = tagSearch.matcher(event?.content ?: "") val matcher = tagSearch.matcher(event?.content() ?: "")
val returningList = mutableSetOf<String>() val returningList = mutableSetOf<String>()
while (matcher.find()) { while (matcher.find()) {
try { 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") { if (tag != null && tag[0] == "p") {
returningList.add(tag[1]) returningList.add(tag[1])
} }
@@ -272,11 +264,11 @@ open class Note(val idHex: String) {
} }
fun directlyCiteUsers(): Set<User> { fun directlyCiteUsers(): Set<User> {
val matcher = tagSearch.matcher(event?.content ?: "") val matcher = tagSearch.matcher(event?.content() ?: "")
val returningList = mutableSetOf<User>() val returningList = mutableSetOf<User>()
while (matcher.find()) { while (matcher.find()) {
try { 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") { if (tag != null && tag[0] == "p") {
LocalCache.checkGetOrCreateUser(tag[1])?.let { LocalCache.checkGetOrCreateUser(tag[1])?.let {
returningList.add(it) returningList.add(it)
@@ -309,7 +301,7 @@ open class Note(val idHex: String) {
} }
fun reactedBy(loggedIn: User, content: String): List<Note> { fun reactedBy(loggedIn: User, content: String): List<Note> {
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 { fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
@@ -11,7 +11,7 @@ class ThreadAssembler {
testedNotes.add(note) 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) if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot)
val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true }
@@ -54,7 +54,7 @@ object UrlCachedPreviewer {
} }
fun preloadPreviewsFor(note: Note) { fun preloadPreviewsFor(note: Note) {
note.event?.content?.let { note.event?.content()?.let {
findUrlsInMessage(it).forEach { findUrlsInMessage(it).forEach {
val removedParamsFromUrl = it.split("?")[0].lowercase() val removedParamsFromUrl = it.split("?")[0].lowercase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) { if (imageExtension.matcher(removedParamsFromUrl).matches()) {
@@ -69,6 +69,8 @@ class User(val pubkeyHex: String) {
fun pubkeyNpub() = pubkey().toNpub() fun pubkeyNpub() = pubkey().toNpub()
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex() fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
override fun toString(): String = pubkeyHex
fun toBestDisplayName(): String { fun toBestDisplayName(): String {
return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex() return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex()
} }
@@ -1,25 +1,16 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.google.gson.Gson import com.google.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.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import fr.acinq.secp256k1.Secp256k1 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.Utils
import nostr.postr.toHex import nostr.postr.toHex
import java.lang.reflect.Type
import java.security.MessageDigest
import java.util.*
open class Event( open class Event(
val id: HexKey, val id: HexKey,
@@ -29,34 +20,27 @@ open class Event(
val tags: List<List<String>>, val tags: List<List<String>>,
val content: String, val content: String,
val sig: HexKey val sig: HexKey
) { ): EventInterface {
fun toJson(): String = gson.toJson(this) override fun id(): HexKey = id
fun generateId(): String { override fun pubKey(): HexKey = pubKey
val rawEvent = listOf(
0,
pubKey,
createdAt,
kind,
tags,
content
)
// GSON decided to hardcode these replacements. override fun createdAt(): Long = createdAt
// 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() override fun kind(): Int = kind
}
override fun tags(): List<List<String>> = 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. * 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())) { if (!id.contentEquals(generateId())) {
throw Exception( throw Exception(
"""|Unexpected ID. """|Unexpected ID.
@@ -70,18 +54,29 @@ open class Event(
} }
} }
fun hasValidSignature(): Boolean { override fun hasValidSignature(): Boolean {
if (!id.contentEquals(generateId())) { if (!id.contentEquals(generateId())) {
return false return false
} }
if (!Secp256k1.get().verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) {
return false return secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
} }
return true 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()
} }
class EventDeserializer : JsonDeserializer<Event> { private class EventDeserializer : JsonDeserializer<Event> {
override fun deserialize( override fun deserialize(
json: JsonElement, json: JsonElement,
typeOfT: Type?, typeOfT: Type?,
@@ -102,7 +97,7 @@ open class Event(
} }
} }
class EventSerializer : JsonSerializer<Event> { private class EventSerializer : JsonSerializer<Event> {
override fun serialize( override fun serialize(
src: Event, src: Event,
typeOfSrc: Type?, typeOfSrc: Type?,
@@ -128,7 +123,7 @@ open class Event(
} }
} }
class ByteArrayDeserializer : JsonDeserializer<ByteArray> { private class ByteArrayDeserializer : JsonDeserializer<ByteArray> {
override fun deserialize( override fun deserialize(
json: JsonElement, json: JsonElement,
typeOfT: Type?, typeOfT: Type?,
@@ -136,7 +131,7 @@ open class Event(
): ByteArray = Hex.decode(json.asString) ): ByteArray = Hex.decode(json.asString)
} }
class ByteArraySerializer : JsonSerializer<ByteArray> { private class ByteArraySerializer : JsonSerializer<ByteArray> {
override fun serialize( override fun serialize(
src: ByteArray, src: ByteArray,
typeOfSrc: Type?, typeOfSrc: Type?,
@@ -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<List<String>>
fun content(): String
fun sig(): HexKey
fun toJson(): String
fun checkSignature()
fun hasValidSignature(): Boolean
}
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import java.math.BigDecimal
class LnZapEvent( class LnZapEvent(
id: HexKey, id: HexKey,
@@ -11,22 +12,29 @@ class LnZapEvent (
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } override fun zappedPost() = tags
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } .filter { it.firstOrNull() == "e" }
.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } override fun zappedAuthor() = tags
.filter { it.firstOrNull() == "p" }
.mapNotNull { it.getOrNull(1) }
fun lnInvoice() = tags.filter { it.firstOrNull() == "bolt11" }.mapNotNull { it.getOrNull(1) }.firstOrNull() override fun taggedAddresses(): List<ATag> = tags
fun preimage() = tags.filter { it.firstOrNull() == "preimage" }.mapNotNull { it.getOrNull(1) }.firstOrNull() .filter { it.firstOrNull() == "a" }
.mapNotNull { it.getOrNull(1) }
.mapNotNull { ATag.parse(it) }
fun description() = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull() override fun amount(): BigDecimal? {
return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
}
// Keeps this as a field because it's a heavier function used everywhere. // Keeps this as a field because it's a heavier function used everywhere.
val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
fun containedPost() = try { override fun containedPost(): Event? = try {
description()?.let { description()?.let {
fromJson(it, Client.lenient) fromJson(it, Client.lenient)
} }
@@ -34,6 +42,16 @@ class LnZapEvent (
null null
} }
private fun lnInvoice(): String? = tags
.filter { it.firstOrNull() == "bolt11" }
.mapNotNull { it.getOrNull(1) }
.firstOrNull()
private fun description(): String? = tags
.filter { it.firstOrNull() == "description" }
.mapNotNull { it.getOrNull(1) }
.firstOrNull()
companion object { companion object {
const val kind = 9735 const val kind = 9735
} }
@@ -0,0 +1,16 @@
package com.vitorpamplona.amethyst.service.model
import java.math.BigDecimal
interface LnZapEventInterface: EventInterface {
fun zappedPost(): List<String>
fun zappedAuthor(): List<String>
fun taggedAddresses(): List<ATag>
fun amount(): BigDecimal?
fun containedPost(): Event?
}
@@ -21,12 +21,12 @@ class LnZapRequestEvent (
companion object { companion object {
const val kind = 9734 const val kind = 9734
fun create(originalNote: Event, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { fun create(originalNote: EventInterface, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
val content = "" val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf( var tags = listOf(
listOf("e", originalNote.id), listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey), listOf("p", originalNote.pubKey()),
listOf("relays") + relays listOf("relays") + relays
) )
if (originalNote is LongTextNoteEvent) { if (originalNote is LongTextNoteEvent) {
@@ -22,18 +22,18 @@ class ReactionEvent (
companion object { companion object {
const val kind = 7 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) 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) 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() 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) { if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
} }
@@ -53,11 +53,11 @@ class ReportEvent (
companion object { companion object {
const val kind = 1984 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 content = ""
val reportPostTag = listOf("e", reportedPost.id, type.name.lowercase()) val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
val reportAuthorTag = listOf("p", reportedPost.pubKey, type.name.lowercase()) val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag) var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
@@ -29,14 +29,14 @@ class RepostEvent (
companion object { companion object {
const val kind = 6 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 content = boostedPost.toJson()
val replyToPost = listOf("e", boostedPost.id) val replyToPost = listOf("e", boostedPost.id())
val replyToAuthor = listOf("p", boostedPost.pubKey) val replyToAuthor = listOf("p", boostedPost.pubKey())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags:List<List<String>> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor)) var tags:List<List<String>> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor))
if (boostedPost is LongTextNoteEvent) { if (boostedPost is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", boostedPost.address().toTag()) ) tags = tags + listOf( listOf("a", boostedPost.address().toTag()) )
@@ -0,0 +1,16 @@
package com.vitorpamplona.amethyst.service.model.zaps
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.LnZapEventInterface
object UserZaps {
fun forProfileFeed(zaps: Map<Note, Note?>?): List<Pair<Note, Note>> {
if (zaps == null) return emptyList()
return (zaps
.filter { it.value != null }
.toList()
.sortedBy { (it.second?.event as? LnZapEventInterface)?.amount() }
.reversed()) as List<Pair<Note, Note>>
}
}
@@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.vitorpamplona.amethyst.service.model.Event 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 * 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() RelayPool.sendFilterOnlyIfDisconnected()
} }
fun send(signedEvent: Event) { fun send(signedEvent: EventInterface) {
RelayPool.send(signedEvent) RelayPool.send(signedEvent)
} }
@@ -4,6 +4,7 @@ import android.util.Log
import com.google.gson.JsonElement import com.google.gson.JsonElement
import java.util.Date import java.util.Date
import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.EventInterface
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@@ -193,7 +194,7 @@ class Relay(
} }
} }
fun send(signedEvent: Event) { fun send(signedEvent: EventInterface) {
if (write) { if (write) {
socket?.send("""["EVENT",${signedEvent.toJson()}]""") socket?.send("""["EVENT",${signedEvent.toJson()}]""")
eventUploadCounter++ eventUploadCounter++
@@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.vitorpamplona.amethyst.service.model.Event 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. * 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() } relays.forEach { it.sendFilterOnlyIfDisconnected() }
} }
fun send(signedEvent: Event) { fun send(signedEvent: EventInterface) {
relays.forEach { it.send(signedEvent) } relays.forEach { it.send(signedEvent) }
} }
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.zaps.UserZaps
object UserProfileZapsFeedFilter: FeedFilter<Pair<Note, Note>>() { object UserProfileZapsFeedFilter: FeedFilter<Pair<Note, Note>>() {
var user: User? = null var user: User? = null
@@ -13,10 +13,6 @@ object UserProfileZapsFeedFilter: FeedFilter<Pair<Note, Note>>() {
} }
override fun feed(): List<Pair<Note, Note>> { override fun feed(): List<Pair<Note, Note>> {
return (user?.zaps return UserZaps.forProfileFeed(user?.zaps)
?.filter { it.value != null }
?.toList()
?.sortedBy { (it.second?.event as? LnZapEvent)?.amount }
?.reversed() ?: emptyList()) as List<Pair<Note, Note>>
} }
} }
@@ -77,7 +77,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
} else if (noteEvent is ChannelMetadataEvent) { } else if (noteEvent is ChannelMetadataEvent) {
"${stringResource(R.string.channel_information_changed_to)} " "${stringResource(R.string.channel_information_changed_to)} "
} else { } else {
noteEvent?.content noteEvent?.content()
} }
channel?.let { channel -> channel?.let { channel ->
var hasNewMessages by remember { mutableStateOf<Boolean>(false) } var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
@@ -127,7 +127,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
LaunchedEffect(key1 = notificationCache, key2 = note) { LaunchedEffect(key1 = notificationCache, key2 = note) {
noteEvent?.let { noteEvent?.let {
hasNewMessages = it.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) hasNewMessages = it.createdAt() > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context)
} }
} }
@@ -265,7 +265,7 @@ fun ChatroomMessageCompose(
eventContent, eventContent,
canPreview, canPreview,
Modifier, Modifier,
note.event?.tags, note.event?.tags(),
backgroundBubbleColor, backgroundBubbleColor,
accountViewModel, accountViewModel,
navController navController
@@ -275,7 +275,7 @@ fun ChatroomMessageCompose(
stringResource(R.string.could_not_decrypt_the_message), stringResource(R.string.could_not_decrypt_the_message),
true, true,
Modifier, Modifier,
note.event?.tags, note.event?.tags(),
backgroundBubbleColor, backgroundBubbleColor,
accountViewModel, accountViewModel,
navController navController
@@ -409,7 +409,7 @@ fun NoteCompose(
eventContent, eventContent,
canPreview = canPreview && !makeItShort, canPreview = canPreview && !makeItShort,
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
noteEvent.tags, noteEvent.tags(),
backgroundColor, backgroundColor,
accountViewModel, accountViewModel,
navController navController
@@ -119,7 +119,7 @@ private fun FeedLoaded(
route = "Room/${userToComposeOn.pubkeyHex}" route = "Room/${userToComposeOn.pubkeyHex}"
} }
notificationCache.cache.markAsRead(route, it.createdAt, context) notificationCache.cache.markAsRead(route, it.createdAt(), context)
} }
} }
markAsRead.value = false markAsRead.value = false
@@ -313,7 +313,7 @@ fun NoteMaster(baseNote: Note,
Row(modifier = Modifier.padding(horizontal = 12.dp)) { Row(modifier = Modifier.padding(horizontal = 12.dp)) {
Column() { Column() {
val eventContent = note.event?.content val eventContent = note.event?.content()
val canPreview = note.author == account.userProfile() val canPreview = note.author == account.userProfile()
|| (note.author?.let { account.userProfile().isFollowing(it) } ?: true ) || (note.author?.let { account.userProfile().isFollowing(it) } ?: true )
@@ -324,7 +324,7 @@ fun NoteMaster(baseNote: Note,
eventContent, eventContent,
canPreview, canPreview,
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
note.event?.tags, note.event?.tags(),
MaterialTheme.colors.background, MaterialTheme.colors.background,
accountViewModel, accountViewModel,
navController navController
@@ -0,0 +1,56 @@
package com.vitorpamplona.amethyst.service.zaps
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.LnZapEventInterface
import com.vitorpamplona.amethyst.service.model.zaps.UserZaps
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert
import org.junit.Test
import java.math.BigDecimal
class UserZapsTest {
@Test
fun nothing() {
Assert.assertEquals(1, 1)
}
@Test
fun user_without_zaps() {
val actual = UserZaps.forProfileFeed(zaps = null)
Assert.assertEquals(emptyList<Pair<Note, Note>>(), actual)
}
@Test
fun avoid_duplicates_with_same_zap_request() {
val zapRequest = mockk<Note>()
val zaps: Map<Note, Note?> = mapOf(
zapRequest to mockZapNoteWith("user-1", amount = 100),
zapRequest to mockZapNoteWith("user-1", amount = 200),
)
val actual = UserZaps.forProfileFeed(zaps)
Assert.assertEquals(1, actual.count())
Assert.assertEquals(zapRequest, actual.first().first)
Assert.assertEquals(
BigDecimal(200),
(actual.first().second.event as LnZapEventInterface).amount()
)
}
private fun mockZapNoteWith(pubkey: HexKey, amount: Int): Note {
val lnZapEvent = mockk<LnZapEventInterface>()
every { lnZapEvent.amount() } returns amount.toBigDecimal()
every { lnZapEvent.pubKey() } returns pubkey
val zapNote = mockk<Note>()
every { zapNote.event } returns lnZapEvent
return zapNote
}
}