Merge branch 'main' into add-ktlint

Conflicts:
	app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
	app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt
	app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt
	app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt
	app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt
	app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt
	app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt
	app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt
	app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt
	app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt
	app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt
	app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt
	app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt
	app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt
This commit is contained in:
Chemaclass
2023-03-07 20:42:38 +01:00
19 changed files with 1735 additions and 1600 deletions

View File

@@ -14,21 +14,27 @@ import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.DeletionEvent
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.DeletionEvent
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import java.io.ByteArrayInputStream
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -37,12 +43,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nostr.postr.toNpub import nostr.postr.toNpub
import java.io.ByteArrayInputStream
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
object LocalCache { object LocalCache {
val metadataParser = jacksonObjectMapper() val metadataParser = jacksonObjectMapper()
@@ -76,7 +77,7 @@ object LocalCache {
} }
fun checkGetOrCreateNote(key: String): Note? { fun checkGetOrCreateNote(key: String): Note? {
if (key.startsWith("naddr1")) { if (ATag.isATag(key)) {
return checkGetOrCreateAddressableNote(key) return checkGetOrCreateAddressableNote(key)
} }
return try { return try {
@@ -107,6 +108,7 @@ object LocalCache {
} }
} }
@Synchronized @Synchronized
fun getOrCreateChannel(key: String): Channel { fun getOrCreateChannel(key: String): Channel {
return channels[key] ?: run { return channels[key] ?: run {
@@ -118,12 +120,11 @@ object LocalCache {
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? { fun checkGetOrCreateAddressableNote(key: String): AddressableNote? {
return try { return try {
val addr = ATag.parse(key) val addr = ATag.parse(key, null) // relay doesn't matter for the index.
if (addr != null) { if (addr != null)
getOrCreateAddressableNote(addr) getOrCreateAddressableNote(addr)
} else { else
null null
}
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Log.e("LocalCache", "Invalid Key to create channel: $key", e) Log.e("LocalCache", "Invalid Key to create channel: $key", e)
null null
@@ -132,10 +133,12 @@ object LocalCache {
@Synchronized @Synchronized
fun getOrCreateAddressableNote(key: ATag): AddressableNote { fun getOrCreateAddressableNote(key: ATag): AddressableNote {
return addressables[key.toNAddr()] ?: run { // we can't use naddr here because naddr might include relay info and
// the preferred relay should not be part of the index.
return addressables[key.toTag()] ?: run {
val answer = AddressableNote(key) val answer = AddressableNote(key)
answer.author = checkGetOrCreateUser(key.pubKeyHex) answer.author = checkGetOrCreateUser(key.pubKeyHex)
addressables.put(key.toNAddr(), answer) addressables.put(key.toTag(), answer)
answer answer
} }
} }
@@ -156,9 +159,9 @@ object LocalCache {
} }
oldUser.updateUserInfo(newUser, event) oldUser.updateUserInfo(newUser, event)
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}") //Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
} else { } else {
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") //Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
} }
} }
@@ -167,6 +170,7 @@ object LocalCache {
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
} }
fun consume(event: TextNoteEvent, relay: Relay? = null) { fun consume(event: TextNoteEvent, relay: Relay? = null) {
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
@@ -192,7 +196,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)
@@ -291,7 +295,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
// Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val awardees = event.awardees().mapNotNull { checkGetOrCreateUser(it) } val awardees = event.awardees().mapNotNull { checkGetOrCreateUser(it) }
@@ -328,6 +332,7 @@ object LocalCache {
citations.add(tag[1]) citations.add(tag[1])
} }
} catch (e: Exception) { } catch (e: Exception) {
} }
} }
return citations return citations
@@ -360,7 +365,7 @@ object LocalCache {
} }
fun consume(event: RecommendRelayEvent) { fun consume(event: RecommendRelayEvent) {
// Log.d("RR", event.toJson()) //Log.d("RR", event.toJson())
} }
fun consume(event: ContactListEvent) { fun consume(event: ContactListEvent) {
@@ -378,7 +383,7 @@ object LocalCache {
getOrCreateUser(pubKey.toHexKey()) getOrCreateUser(pubKey.toHexKey())
} catch (e: Exception) { } catch (e: Exception) {
Log.w("ContactList Parser", "Ignoring: Could not parse Hex key: ${it.pubKeyHex} in ${event.toJson()}") Log.w("ContactList Parser", "Ignoring: Could not parse Hex key: ${it.pubKeyHex} in ${event.toJson()}")
// e.printStackTrace() //e.printStackTrace()
null null
} }
}.filterNotNull().toSet(), }.filterNotNull().toSet(),
@@ -397,7 +402,7 @@ object LocalCache {
user.updateRelays(relays) user.updateRelays(relays)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w("Relay List Parser", "Relay import issue ${e.message}", e) Log.w("Relay List Parser","Relay import issue ${e.message}", e)
e.printStackTrace() e.printStackTrace()
} }
@@ -419,7 +424,7 @@ object LocalCache {
val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) } val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) }
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") //Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateNote(it) } val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateNote(it) }
val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateUser(it) } val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateUser(it) }
@@ -478,7 +483,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
// Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
@@ -519,7 +524,7 @@ object LocalCache {
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
// Log.d("RE", "New Reaction ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("RE", "New Reaction ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users. // Adds notifications to users.
mentions.forEach { mentions.forEach {
@@ -542,8 +547,8 @@ object LocalCache {
} }
} }
if (event.content == "!" || // nostr_console hide. if (event.content == "!" // nostr_console hide.
event.content == "\u26A0\uFE0F" // Warning sign || event.content == "\u26A0\uFE0F" // Warning sign
) { ) {
// Counts the replies // Counts the replies
repliesTo.forEach { repliesTo.forEach {
@@ -570,7 +575,7 @@ object LocalCache {
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
// Log.d("RP", "New Report ${event.content} by ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("RP", "New Report ${event.content} by ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users. // Adds notifications to users.
if (repliesTo.isEmpty()) { if (repliesTo.isEmpty()) {
mentions.forEach { mentions.forEach {
@@ -583,7 +588,7 @@ object LocalCache {
} }
fun consume(event: ChannelCreateEvent) { fun consume(event: ChannelCreateEvent) {
// Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") //Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
// new event // new event
val oldChannel = getOrCreateChannel(event.id) val oldChannel = getOrCreateChannel(event.id)
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
@@ -604,7 +609,7 @@ object LocalCache {
fun consume(event: ChannelMetadataEvent) { fun consume(event: ChannelMetadataEvent) {
val channelId = event.channel() val channelId = event.channel()
// Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}") //Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
if (channelId.isNullOrBlank()) return if (channelId.isNullOrBlank()) return
// new event // new event
@@ -621,7 +626,7 @@ object LocalCache {
refreshObservers() refreshObservers()
} }
} else { } else {
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") //Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
} }
} }
@@ -659,7 +664,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 {
@@ -678,9 +683,11 @@ object LocalCache {
} }
fun consume(event: ChannelHideMessageEvent) { fun consume(event: ChannelHideMessageEvent) {
} }
fun consume(event: ChannelMuteUserEvent) { fun consume(event: ChannelMuteUserEvent) {
} }
fun consume(event: LnZapEvent) { fun consume(event: LnZapEvent) {
@@ -700,11 +707,11 @@ object LocalCache {
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
if (zapRequest == null) { if (zapRequest == null) {
Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}") Log.e("ZP","Zap Request not found. Unable to process Zap {${event.toJson()}}")
return return
} }
// Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users. // Adds notifications to users.
mentions.forEach { mentions.forEach {
@@ -735,7 +742,7 @@ object LocalCache {
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
// Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users. // Adds notifications to users.
mentions.forEach { mentions.forEach {
@@ -755,31 +762,31 @@ object LocalCache {
fun findUsersStartingWith(username: String): List<User> { fun findUsersStartingWith(username: String): List<User> {
return users.values.filter { return users.values.filter {
(it.anyNameStartsWith(username)) || (it.anyNameStartsWith(username))
it.pubkeyHex.startsWith(username, true) || || it.pubkeyHex.startsWith(username, true)
it.pubkeyNpub().startsWith(username, true) || it.pubkeyNpub().startsWith(username, true)
} }
} }
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 {
(it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false || (it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false
(it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false || || (it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false
(it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false || || (it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false
it.idHex.startsWith(text, true) || it.idHex.startsWith(text, true)
} }
} }
fun findChannelsStartingWith(text: String): List<Channel> { fun findChannelsStartingWith(text: String): List<Channel> {
return channels.values.filter { return channels.values.filter {
it.anyNameStartsWith(text) || it.anyNameStartsWith(text)
it.idHex.startsWith(text, true) || || it.idHex.startsWith(text, true)
it.idNote().startsWith(text, true) || it.idNote().startsWith(text, true)
} }
} }
@@ -896,7 +903,7 @@ object LocalCache {
} }
} }
class LocalCacheLiveData(val cache: LocalCache) : LiveData<LocalCacheState>(LocalCacheState(cache)) { class LocalCacheLiveData(val cache: LocalCache): LiveData<LocalCacheState>(LocalCacheState(cache)) {
// Refreshes observers in batches. // Refreshes observers in batches.
var handlerWaiting = AtomicBoolean() var handlerWaiting = AtomicBoolean()
@@ -923,4 +930,6 @@ class LocalCacheLiveData(val cache: LocalCache) : LiveData<LocalCacheState>(Loca
} }
} }
class LocalCacheState(val cache: LocalCache) class LocalCacheState(val cache: LocalCache) {
}

View File

@@ -6,13 +6,6 @@ import com.vitorpamplona.amethyst.service.model.*
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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal import java.math.BigDecimal
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
@@ -20,10 +13,18 @@ import java.time.format.DateTimeFormatter
import java.util.Date import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
class AddressableNote(val address: ATag) : Note(address.toNAddr()) {
class AddressableNote(val address: ATag): Note(address.toTag()) {
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
@@ -61,9 +62,9 @@ open class Note(val idHex: String) {
fun channel(): Channel? { fun channel(): Channel? {
val channelHex = val channelHex =
(event as? ChannelMessageEvent)?.channel() (event as? ChannelMessageEvent)?.channel() ?:
?: (event as? ChannelMetadataEvent)?.channel() (event as? ChannelMetadataEvent)?.channel() ?:
?: (event as? ChannelCreateEvent)?.let { it.id } (event as? ChannelCreateEvent)?.let { it.id }
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
} }
@@ -136,7 +137,7 @@ open class Note(val idHex: String) {
fun removeReport(deleteNote: Note) { fun removeReport(deleteNote: Note) {
val author = deleteNote.author ?: return val author = deleteNote.author ?: return
if (author in reports.keys && reports[author]?.contains(deleteNote) == true) { if (author in reports.keys && reports[author]?.contains(deleteNote) == true ) {
reports[author]?.let { reports[author]?.let {
reports = reports + Pair(author, it.minus(deleteNote)) reports = reports + Pair(author, it.minus(deleteNote))
liveSet?.reports?.invalidateData() liveSet?.reports?.invalidateData()
@@ -155,6 +156,7 @@ open class Note(val idHex: String) {
} }
} }
fun addBoost(note: Note) { fun addBoost(note: Note) {
if (note !in boosts) { if (note !in boosts) {
boosts = boosts + note boosts = boosts + note
@@ -238,13 +240,11 @@ open class Note(val idHex: String) {
} }
fun hasAnyReports(): Boolean { fun hasAnyReports(): Boolean {
val dayAgo = Date().time / 1000 - 24 * 60 * 60 val dayAgo = Date().time / 1000 - 24*60*60
return reports.isNotEmpty() || return reports.isNotEmpty() ||
( (author?.reports?.values?.filter {
author?.reports?.values?.filter { it.firstOrNull { ( it.createdAt() ?: 0 ) > dayAgo } != null
it.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null }?.isNotEmpty() ?: false)
}?.isNotEmpty() ?: false
)
} }
fun directlyCiteUsersHex(): Set<HexKey> { fun directlyCiteUsersHex(): Set<HexKey> {
@@ -257,6 +257,7 @@ open class Note(val idHex: String) {
returningList.add(tag[1]) returningList.add(tag[1])
} }
} catch (e: Exception) { } catch (e: Exception) {
} }
} }
return returningList return returningList
@@ -273,17 +274,18 @@ open class Note(val idHex: String) {
returningList.add(it) returningList.add(it)
} }
} }
} catch (_: Exception) { } catch (e: Exception) {
} }
} }
return returningList return returningList
} }
fun directlyCites(userProfile: User): Boolean { fun directlyCites(userProfile: User): Boolean {
return author == userProfile || return author == userProfile
(userProfile in directlyCiteUsers()) || || (userProfile in directlyCiteUsers())
(event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) || || (event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true)
(event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) || (event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true)
} }
fun isNewThread(): Boolean { fun isNewThread(): Boolean {
@@ -304,7 +306,7 @@ open class Note(val idHex: String) {
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
val currentTime = Date().time / 1000 val currentTime = Date().time / 1000
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5) } != null // 5 minute protection return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection
} }
fun boostedBy(loggedIn: User): List<Note> { fun boostedBy(loggedIn: User): List<Note> {
@@ -327,6 +329,7 @@ open class Note(val idHex: String) {
} }
} }
class NoteLiveSet(u: Note) { class NoteLiveSet(u: Note) {
// Observers line up here. // Observers line up here.
val metadata: NoteLiveData = NoteLiveData(u) val metadata: NoteLiveData = NoteLiveData(u)
@@ -339,17 +342,17 @@ class NoteLiveSet(u: Note) {
val zaps: NoteLiveData = NoteLiveData(u) val zaps: NoteLiveData = NoteLiveData(u)
fun isInUse(): Boolean { fun isInUse(): Boolean {
return metadata.hasObservers() || return metadata.hasObservers()
reactions.hasObservers() || || reactions.hasObservers()
boosts.hasObservers() || || boosts.hasObservers()
replies.hasObservers() || || replies.hasObservers()
reports.hasObservers() || || reports.hasObservers()
relays.hasObservers() || || relays.hasObservers()
zaps.hasObservers() || zaps.hasObservers()
} }
} }
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) { class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches. // Refreshes observers in batches.
var handlerWaiting = AtomicBoolean() var handlerWaiting = AtomicBoolean()
@@ -381,6 +384,7 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
} else { } else {
NostrSingleEventDataSource.add(note) NostrSingleEventDataSource.add(note)
} }
} }
override fun onInactive() { override fun onInactive() {

View File

@@ -19,11 +19,10 @@ class ThreadAssembler {
// recursive // recursive
val roots = note.replyTo?.map { val roots = note.replyTo?.map {
if (it !in testedNotes) { if (it !in testedNotes)
searchRoot(it, testedNotes) searchRoot(it, testedNotes)
} else { else
null null
}
}?.filterNotNull() }?.filterNotNull()
if (roots != null && roots.isNotEmpty()) { if (roots != null && roots.isNotEmpty()) {
@@ -36,17 +35,17 @@ class ThreadAssembler {
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
fun findThreadFor(noteId: String): Set<Note> { fun findThreadFor(noteId: String): Set<Note> {
val (result, elapsed) = measureTimedValue { val (result, elapsed) = measureTimedValue {
val note = if (noteId.startsWith("naddr")) { val note = if (noteId.contains(":")) {
val aTag = ATag.parse(noteId) val aTag = ATag.parse(noteId, null)
if (aTag != null) { if (aTag != null)
LocalCache.getOrCreateAddressableNote(aTag) LocalCache.getOrCreateAddressableNote(aTag)
} else { else
return emptySet() return emptySet()
}
} else { } else {
LocalCache.getOrCreateNote(noteId) LocalCache.getOrCreateNote(noteId)
} }
if (note.event != null) { if (note.event != null) {
val thread = mutableSetOf<Note>() val thread = mutableSetOf<Note>()
@@ -60,7 +59,7 @@ class ThreadAssembler {
} }
} }
println("Model Refresh: Thread loaded in $elapsed") println("Model Refresh: Thread loaded in ${elapsed}")
return result return result
} }

View File

@@ -11,7 +11,7 @@ class Nip19 {
USER, NOTE, RELAY, ADDRESS USER, NOTE, RELAY, ADDRESS
} }
data class Return(val type: Type, val hex: String) data class Return(val type: Type, val hex: String, val relay: String?)
fun uriToRoute(uri: String?): Return? { fun uriToRoute(uri: String?): Return? {
try { try {
@@ -32,36 +32,46 @@ class Nip19 {
return naddr(bytes) return naddr(bytes)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
println("Issue trying to Decode NIP19 $uri: ${e.message}") println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
} }
return null return null
} }
private fun npub(bytes: ByteArray): Return { private fun npub(bytes: ByteArray): Return {
return Return(Type.USER, bytes.toHexKey()) return Return(Type.USER, bytes.toHexKey(), null)
} }
private fun note(bytes: ByteArray): Return { private fun note(bytes: ByteArray): Return {
return Return(Type.NOTE, bytes.toHexKey()) return Return(Type.NOTE, bytes.toHexKey(), null);
} }
private fun nprofile(bytes: ByteArray): Return? { private fun nprofile(bytes: ByteArray): Return? {
val hex = parseTLV(bytes) val tlv = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
?.get(0) ?.get(0)
?.toHexKey() ?: return null ?.toHexKey() ?: return null
return Return(Type.USER, hex) val relay = tlv.get(NIP19TLVTypes.RELAY.id)
?.get(0)
?.toString(Charsets.UTF_8)
return Return(Type.USER, hex, relay)
} }
private fun nevent(bytes: ByteArray): Return? { private fun nevent(bytes: ByteArray): Return? {
val hex = parseTLV(bytes) val tlv = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
?.get(0) ?.get(0)
?.toHexKey() ?: return null ?.toHexKey() ?: return null
return Return(Type.USER, hex) val relay = tlv.get(NIP19TLVTypes.RELAY.id)
?.get(0)
?.toString(Charsets.UTF_8)
return Return(Type.USER, hex, relay)
} }
private fun nrelay(bytes: ByteArray): Return? { private fun nrelay(bytes: ByteArray): Return? {
@@ -70,7 +80,7 @@ class Nip19 {
?.get(0) ?.get(0)
?.toString(Charsets.UTF_8) ?: return null ?.toString(Charsets.UTF_8) ?: return null
return Return(Type.RELAY, relayUrl) return Return(Type.RELAY, relayUrl, null)
} }
private fun naddr(bytes: ByteArray): Return? { private fun naddr(bytes: ByteArray): Return? {
@@ -92,7 +102,7 @@ class Nip19 {
?.get(0) ?.get(0)
?.let { toInt32(it) } ?.let { toInt32(it) }
return Return(Type.ADDRESS, "$kind:$author:$d") return Return(Type.ADDRESS, "$kind:$author:$d", relay)
} }
} }

View File

@@ -4,16 +4,16 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") { object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String?) { fun loadUserProfile(userId: String?) {
@@ -84,7 +84,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(BadgeProfilesEvent.kind), kinds = listOf(BadgeProfilesEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)), authors = listOf(it.pubkeyHex),
limit = 1 limit = 1
) )
) )

View File

@@ -11,38 +11,47 @@ import nostr.postr.Bech32
import nostr.postr.bechToBytes import nostr.postr.bechToBytes
import nostr.postr.toByteArray import nostr.postr.toByteArray
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val relay: String?) {
fun toTag() = "$kind:$pubKeyHex:$dTag" fun toTag() = "$kind:$pubKeyHex:$dTag"
fun toNAddr(): String { fun toNAddr(): String {
val kind = kind.toByteArray() val kind = kind.toByteArray()
val addr = pubKeyHex.toByteArray() val author = pubKeyHex.toByteArray()
val dTag = dTag.toByteArray(Charsets.UTF_8) val dTag = dTag.toByteArray(Charsets.UTF_8)
val relay = relay?.toByteArray(Charsets.UTF_8)
val fullArray = var fullArray =
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag + byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag
byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr +
if (relay != null)
fullArray = fullArray + byteArrayOf(NIP19TLVTypes.RELAY.id, relay.size.toByte()) + relay
fullArray = fullArray +
byteArrayOf(NIP19TLVTypes.AUTHOR.id, author.size.toByte()) + author +
byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind
return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32) return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32)
} }
companion object { companion object {
fun parse(address: String): ATag? { fun isATag(key: String): Boolean {
return if (address.startsWith("naddr") || address.startsWith("nostr:naddr")) { return key.startsWith("naddr1") || key.contains(":")
parseNAddr(address)
} else {
parseAtag(address)
}
} }
fun parseAtag(atag: String): ATag? { fun parse(address: String, relay: String?): ATag? {
return if (address.startsWith("naddr") || address.startsWith("nostr:naddr"))
parseNAddr(address)
else
parseAtag(address, relay)
}
fun parseAtag(atag: String, relay: String?): ATag? {
return try { return try {
val parts = atag.split(":") val parts = atag.split(":")
Hex.decode(parts[1]) Hex.decode(parts[1])
ATag(parts[0].toInt(), parts[1], parts[2]) ATag(parts[0].toInt(), parts[1], parts[2], relay)
} catch (t: Throwable) { } catch (t: Throwable) {
Log.w("ATag", "Error parsing A Tag: $atag: ${t.message}") Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}")
null null
} }
} }
@@ -58,13 +67,13 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey()
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
if (kind != null && author != null) { if (kind != null && author != null)
return ATag(kind, author, d) return ATag(kind, author, d, relay)
}
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.w("ATag", "Issue trying to Decode NIP19 $this: ${e.message}") Log.w( "ATag", "Issue trying to Decode NIP19 ${this}: ${e.message}")
// e.printStackTrace() //e.printStackTrace()
} }
return null return null

View File

@@ -11,7 +11,12 @@ class BadgeAwardEvent(
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun awardees() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun awardees() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun awardDefinition() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } fun awardDefinition() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object { companion object {
const val kind = 8 const val kind = 8

View File

@@ -11,7 +11,7 @@ class BadgeDefinitionEvent(
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag()) fun address() = ATag(kind, pubKey, dTag(), null)
fun name() = tags.filter { it.firstOrNull() == "name" }.mapNotNull { it.getOrNull(1) }.firstOrNull() fun name() = tags.filter { it.firstOrNull() == "name" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun thumb() = tags.filter { it.firstOrNull() == "thumb" }.mapNotNull { it.getOrNull(1) }.firstOrNull() fun thumb() = tags.filter { it.firstOrNull() == "thumb" }.mapNotNull { it.getOrNull(1) }.firstOrNull()

View File

@@ -11,10 +11,15 @@ class BadgeProfilesEvent(
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag()) fun address() = ATag(kind, pubKey, dTag(), null)
companion object { companion object {
const val kind = 30008 const val kind = 30008

View File

@@ -12,7 +12,7 @@ class LnZapEvent(
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) { ): LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) {
override fun zappedPost() = tags override fun zappedPost() = tags
.filter { it.firstOrNull() == "e" } .filter { it.firstOrNull() == "e" }
@@ -24,8 +24,12 @@ class LnZapEvent(
override fun taggedAddresses(): List<ATag> = tags override fun taggedAddresses(): List<ATag> = tags
.filter { it.firstOrNull() == "a" } .filter { it.firstOrNull() == "a" }
.mapNotNull { it.getOrNull(1) } .mapNotNull {
.mapNotNull { ATag.parse(it) } val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
override fun amount(): BigDecimal? { override fun amount(): BigDecimal? {
return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }

View File

@@ -2,20 +2,26 @@ package com.vitorpamplona.amethyst.service.model
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 nostr.postr.Utils
import java.util.Date import java.util.Date
import nostr.postr.Utils
import nostr.postr.toHex
class LnZapRequestEvent( class LnZapRequestEvent (
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object { companion object {
const val kind = 9734 const val kind = 9734
@@ -29,7 +35,7 @@ class LnZapRequestEvent(
listOf("relays") + relays listOf("relays") + relays
) )
if (originalNote is LongTextNoteEvent) { if (originalNote is LongTextNoteEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag())) tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
} }
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)

View File

@@ -17,7 +17,7 @@ class LongTextNoteEvent(
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag()) fun address() = ATag(kind, pubKey, dTag(), null)
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()

View File

@@ -2,21 +2,27 @@ package com.vitorpamplona.amethyst.service.model
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 nostr.postr.Utils
import java.util.Date import java.util.Date
import nostr.postr.Utils
import nostr.postr.toHex
class ReactionEvent( class ReactionEvent (
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object { companion object {
const val kind = 7 const val kind = 7
@@ -32,9 +38,9 @@ class ReactionEvent(
fun create(content: String, originalNote: EventInterface, 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()) )
} }
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)

View File

@@ -2,20 +2,21 @@ package com.vitorpamplona.amethyst.service.model
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 nostr.postr.Utils
import java.util.Date import java.util.Date
import nostr.postr.Utils
import nostr.postr.toHex
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType) data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
// NIP 56 event. // NIP 56 event.
class ReportEvent( class ReportEvent (
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
private fun defaultReportType(): ReportType { private fun defaultReportType(): ReportType {
// Works with old and new structures for report. // Works with old and new structures for report.
@@ -34,7 +35,7 @@ class ReportEvent(
.map { .map {
ReportedKey( ReportedKey(
it[1], it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) } ?: defaultReportType() it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType()
) )
} }
@@ -43,11 +44,16 @@ class ReportEvent(
.map { .map {
ReportedKey( ReportedKey(
it[1], it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) } ?: defaultReportType() it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType()
) )
} }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object { companion object {
const val kind = 1984 const val kind = 1984
@@ -59,10 +65,10 @@ class ReportEvent(
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)
if (reportedPost is LongTextNoteEvent) { if (reportedPost is LongTextNoteEvent) {
tags = tags + listOf(listOf("a", reportedPost.address().toTag())) tags = tags + listOf( listOf("a", reportedPost.address().toTag()) )
} }
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
@@ -76,7 +82,7 @@ class ReportEvent(
val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase()) val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags: List<List<String>> = listOf(reportAuthorTag) val tags:List<List<String>> = listOf(reportAuthorTag)
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
@@ -89,6 +95,6 @@ class ReportEvent(
SPAM, SPAM,
IMPERSONATION, IMPERSONATION,
NUDITY, NUDITY,
PROFANITY PROFANITY,
} }
} }

View File

@@ -3,21 +3,27 @@ package com.vitorpamplona.amethyst.service.model
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 com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils
import java.util.Date import java.util.Date
import nostr.postr.Utils
class RepostEvent( class RepostEvent (
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun containedPost() = try { fun containedPost() = try {
fromJson(content, Client.lenient) fromJson(content, Client.lenient)
@@ -35,10 +41,10 @@ class RepostEvent(
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()) )
} }
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)

View File

@@ -14,7 +14,13 @@ class TextNoteEvent(
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
companion object { companion object {

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@@ -27,11 +28,28 @@ fun ClickableRoute(
val text = user.toBestDisplayName() val text = user.toBestDisplayName()
ClickableText( ClickableText(
text = AnnotatedString("@$text "), text = AnnotatedString("@${text} "),
onClick = { navController.navigate(route) }, onClick = { navController.navigate(route) },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
) )
} else if (nip19.type == Nip19.Type.ADDRESS) {
val noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
if (noteBase == null) {
Text(
"@${nip19.hex} "
)
} else { } else {
val noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Note/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
}
} else if (nip19.type == Nip19.Type.NOTE) {
val noteBase = LocalCache.getOrCreateNote(nip19.hex) val noteBase = LocalCache.getOrCreateNote(nip19.hex)
val noteState by noteBase.live().metadata.observeAsState() val noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return val note = noteState?.note ?: return
@@ -55,5 +73,9 @@ fun ClickableRoute(
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
) )
} }
} else {
Text(
"@${nip19.hex} "
)
} }
} }

View File

@@ -8,20 +8,28 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -33,9 +41,9 @@ import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.Nip19 import com.vitorpamplona.amethyst.service.Nip19
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import java.net.MalformedURLException import java.net.MalformedURLException
@@ -71,8 +79,9 @@ fun RichTextViewer(
tags: List<List<String>>?, tags: List<List<String>>?,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController navController: NavController,
) { ) {
val myMarkDownStyle = RichTextStyle().resolveDefaults().copy( val myMarkDownStyle = RichTextStyle().resolveDefaults().copy(
codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy( codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy(
textStyle = TextStyle( textStyle = TextStyle(
@@ -104,25 +113,27 @@ fun RichTextViewer(
) )
Column(modifier = modifier.animateContentSize()) { Column(modifier = modifier.animateContentSize()) {
if (content.startsWith("# ") ||
content.contains("##") || if ( content.startsWith("# ")
content.contains("**") || || content.contains("##")
content.contains("__") || || content.contains("**")
content.contains("```") || content.contains("__")
|| content.contains("```")
) { ) {
MaterialRichText( MaterialRichText(
style = myMarkDownStyle style = myMarkDownStyle,
) { ) {
Markdown( Markdown(
content = content, content = content,
markdownParseOptions = MarkdownParseOptions.Default markdownParseOptions = MarkdownParseOptions.Default,
) )
} }
} else { } else {
// FlowRow doesn't work well with paragraphs. So we need to split them // FlowRow doesn't work well with paragraphs. So we need to split them
content.split('\n').forEach { paragraph -> content.split('\n').forEach { paragraph ->
FlowRow() { FlowRow() {
val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' ') val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' ');
s.forEach { word: String -> s.forEach { word: String ->
if (canPreview) { if (canPreview) {
// Explicit URL // Explicit URL
@@ -151,7 +162,7 @@ fun RichTextViewer(
} else { } else {
Text( Text(
text = "$word ", text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
) )
} }
} else { } else {
@@ -170,7 +181,7 @@ fun RichTextViewer(
} else { } else {
Text( Text(
text = "$word ", text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
) )
} }
} }
@@ -185,16 +196,19 @@ private fun isArabic(text: String): Boolean {
return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' } return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' }
} }
fun isBechLink(word: String): Boolean { fun isBechLink(word: String): Boolean {
return word.startsWith("nostr:", true) || return word.startsWith("nostr:", true)
word.startsWith("npub1", true) || || word.startsWith("npub1", true)
word.startsWith("note1", true) || || word.startsWith("naddr1", true)
word.startsWith("nprofile1", true) || || word.startsWith("note1", true)
word.startsWith("nevent1", true) || || word.startsWith("nprofile1", true)
word.startsWith("@npub1", true) || || word.startsWith("nevent1", true)
word.startsWith("@note1", true) || || word.startsWith("@npub1", true)
word.startsWith("@nprofile1", true) || || word.startsWith("@note1", true)
word.startsWith("@nevent1", true) || word.startsWith("@addr1", true)
|| word.startsWith("@nprofile1", true)
|| word.startsWith("@nevent1", true)
} }
@Composable @Composable
@@ -204,7 +218,7 @@ fun BechLink(word: String, navController: NavController) {
} else if (word.startsWith("@")) { } else if (word.startsWith("@")) {
word.replaceFirst("@", "nostr:") word.replaceFirst("@", "nostr:")
} else { } else {
"nostr:$word" "nostr:${word}"
} }
val nip19Route = try { val nip19Route = try {
@@ -220,6 +234,7 @@ fun BechLink(word: String, navController: NavController) {
} }
} }
@Composable @Composable
fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) { fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) {
val matcher = tagIndex.matcher(word) val matcher = tagIndex.matcher(word)
@@ -228,7 +243,7 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
matcher.find() matcher.find()
matcher.group(1).toInt() matcher.group(1).toInt()
} catch (e: Exception) { } catch (e: Exception) {
println("Couldn't link tag $word") println("Couldn't link tag ${word}")
null null
} }
@@ -279,8 +294,7 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
// if here the tag is not a valid Nostr Hex // if here the tag is not a valid Nostr Hex
Text(text = "$word ") Text(text = "$word ")
} }
} else { } else
Text(text = "$word ") Text(text = "$word ")
} }
}
} }

View File

@@ -18,15 +18,39 @@ class NIP19ParserTest {
assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex) assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex)
} }
@Test
fun nAddrParse3() {
val result = Nip19().uriToRoute("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38")
assertEquals(Nip19.Type.ADDRESS, result?.type)
assertEquals("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", result?.hex)
assertEquals("wss://relay.damus.io", result?.relay)
}
@Test
fun nAddrATagParse3() {
val address = ATag.parse("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", "wss://relay.damus.io")
assertEquals(30023, address?.kind)
assertEquals("d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", address?.pubKeyHex)
assertEquals("89de7920", address?.dTag)
assertEquals("wss://relay.damus.io" , address?.relay)
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address?.toNAddr())
}
@Test @Test
fun nAddrFormatter() { fun nAddrFormatter() {
val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "") val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "", null)
assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr()) assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr())
} }
@Test @Test
fun nAddrFormatter2() { fun nAddrFormatter2() {
val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard") val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard", null)
assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr()) assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr())
} }
@Test
fun nAddrFormatter3() {
val address = ATag(30023, "d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", "89de7920", "wss://relay.damus.io")
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr())
}
} }