mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-10 22:43:44 +02:00
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:
@@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
@@ -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() {
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
@@ -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) }
|
||||||
|
@@ -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)
|
||||||
|
@@ -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()
|
||||||
|
@@ -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)
|
||||||
|
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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} "
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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 ")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user