Merge pull request #205 from Chemaclass/refactor-UserProfileZapsFeedFilter
Refactor UserProfileZapsFeedFilter
This commit is contained in:
Generated
+1
-1
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user