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
@@ -14,21 +14,27 @@ import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
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.LnZapRequestEvent
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.RecommendRelayEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
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.relays.Relay
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.Dispatchers
import kotlinx.coroutines.Job
@@ -37,12 +43,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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 {
val metadataParser = jacksonObjectMapper()
@@ -76,7 +77,7 @@ object LocalCache {
}
fun checkGetOrCreateNote(key: String): Note? {
if (key.startsWith("naddr1")) {
if (ATag.isATag(key)) {
return checkGetOrCreateAddressableNote(key)
}
return try {
@@ -107,6 +108,7 @@ object LocalCache {
}
}
@Synchronized
fun getOrCreateChannel(key: String): Channel {
return channels[key] ?: run {
@@ -118,12 +120,11 @@ object LocalCache {
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? {
return try {
val addr = ATag.parse(key)
if (addr != null) {
val addr = ATag.parse(key, null) // relay doesn't matter for the index.
if (addr != null)
getOrCreateAddressableNote(addr)
} else {
else
null
}
} catch (e: IllegalArgumentException) {
Log.e("LocalCache", "Invalid Key to create channel: $key", e)
null
@@ -132,10 +133,12 @@ object LocalCache {
@Synchronized
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)
answer.author = checkGetOrCreateUser(key.pubKeyHex)
addressables.put(key.toNAddr(), answer)
addressables.put(key.toTag(), answer)
answer
}
}
@@ -167,6 +170,7 @@ object LocalCache {
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
}
fun consume(event: TextNoteEvent, relay: Relay? = null) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
@@ -328,6 +332,7 @@ object LocalCache {
citations.add(tag[1])
}
} catch (e: Exception) {
}
}
return citations
@@ -542,8 +547,8 @@ object LocalCache {
}
}
if (event.content == "!" || // nostr_console hide.
event.content == "\u26A0\uFE0F" // Warning sign
if (event.content == "!" // nostr_console hide.
|| event.content == "\u26A0\uFE0F" // Warning sign
) {
// Counts the replies
repliesTo.forEach {
@@ -678,9 +683,11 @@ object LocalCache {
}
fun consume(event: ChannelHideMessageEvent) {
}
fun consume(event: ChannelMuteUserEvent) {
}
fun consume(event: LnZapEvent) {
@@ -755,31 +762,31 @@ object LocalCache {
fun findUsersStartingWith(username: String): List<User> {
return users.values.filter {
(it.anyNameStartsWith(username)) ||
it.pubkeyHex.startsWith(username, true) ||
it.pubkeyNpub().startsWith(username, true)
(it.anyNameStartsWith(username))
|| it.pubkeyHex.startsWith(username, true)
|| it.pubkeyNpub().startsWith(username, true)
}
}
fun findNotesStartingWith(text: String): List<Note> {
return notes.values.filter {
(it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false) ||
(it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false) ||
it.idHex.startsWith(text, true) ||
it.idNote().startsWith(text, true)
(it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false)
|| (it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false)
|| it.idHex.startsWith(text, true)
|| it.idNote().startsWith(text, true)
} + addressables.values.filter {
(it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false ||
(it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false ||
(it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false ||
it.idHex.startsWith(text, true)
(it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false
|| (it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false
|| (it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false
|| it.idHex.startsWith(text, true)
}
}
fun findChannelsStartingWith(text: String): List<Channel> {
return channels.values.filter {
it.anyNameStartsWith(text) ||
it.idHex.startsWith(text, true) ||
it.idNote().startsWith(text, true)
it.anyNameStartsWith(text)
|| it.idHex.startsWith(text, true)
|| it.idNote().startsWith(text, true)
}
}
@@ -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.ui.note.toShortenHex
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.time.Instant
import java.time.ZoneId
@@ -20,10 +13,18 @@ import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean
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]+)\\]")
class AddressableNote(val address: ATag) : Note(address.toNAddr()) {
class AddressableNote(val address: ATag): Note(address.toTag()) {
override fun idNote() = address.toNAddr()
override fun idDisplayNote() = idNote().toShortenHex()
override fun address() = address
@@ -61,9 +62,9 @@ open class Note(val idHex: String) {
fun channel(): Channel? {
val channelHex =
(event as? ChannelMessageEvent)?.channel()
?: (event as? ChannelMetadataEvent)?.channel()
?: (event as? ChannelCreateEvent)?.let { it.id }
(event as? ChannelMessageEvent)?.channel() ?:
(event as? ChannelMetadataEvent)?.channel() ?:
(event as? ChannelCreateEvent)?.let { it.id }
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
}
@@ -155,6 +156,7 @@ open class Note(val idHex: String) {
}
}
fun addBoost(note: Note) {
if (note !in boosts) {
boosts = boosts + note
@@ -240,11 +242,9 @@ open class Note(val idHex: String) {
fun hasAnyReports(): Boolean {
val dayAgo = Date().time / 1000 - 24*60*60
return reports.isNotEmpty() ||
(
author?.reports?.values?.filter {
(author?.reports?.values?.filter {
it.firstOrNull { ( it.createdAt() ?: 0 ) > dayAgo } != null
}?.isNotEmpty() ?: false
)
}?.isNotEmpty() ?: false)
}
fun directlyCiteUsersHex(): Set<HexKey> {
@@ -257,6 +257,7 @@ open class Note(val idHex: String) {
returningList.add(tag[1])
}
} catch (e: Exception) {
}
}
return returningList
@@ -273,17 +274,18 @@ open class Note(val idHex: String) {
returningList.add(it)
}
}
} catch (_: Exception) {
} catch (e: Exception) {
}
}
return returningList
}
fun directlyCites(userProfile: User): Boolean {
return author == userProfile ||
(userProfile in directlyCiteUsers()) ||
(event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) ||
(event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true)
return author == userProfile
|| (userProfile in directlyCiteUsers())
|| (event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true)
|| (event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true)
}
fun isNewThread(): Boolean {
@@ -327,6 +329,7 @@ open class Note(val idHex: String) {
}
}
class NoteLiveSet(u: Note) {
// Observers line up here.
val metadata: NoteLiveData = NoteLiveData(u)
@@ -339,13 +342,13 @@ class NoteLiveSet(u: Note) {
val zaps: NoteLiveData = NoteLiveData(u)
fun isInUse(): Boolean {
return metadata.hasObservers() ||
reactions.hasObservers() ||
boosts.hasObservers() ||
replies.hasObservers() ||
reports.hasObservers() ||
relays.hasObservers() ||
zaps.hasObservers()
return metadata.hasObservers()
|| reactions.hasObservers()
|| boosts.hasObservers()
|| replies.hasObservers()
|| reports.hasObservers()
|| relays.hasObservers()
|| zaps.hasObservers()
}
}
@@ -381,6 +384,7 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
} else {
NostrSingleEventDataSource.add(note)
}
}
override fun onInactive() {
@@ -19,11 +19,10 @@ class ThreadAssembler {
// recursive
val roots = note.replyTo?.map {
if (it !in testedNotes) {
if (it !in testedNotes)
searchRoot(it, testedNotes)
} else {
else
null
}
}?.filterNotNull()
if (roots != null && roots.isNotEmpty()) {
@@ -36,17 +35,17 @@ class ThreadAssembler {
@OptIn(ExperimentalTime::class)
fun findThreadFor(noteId: String): Set<Note> {
val (result, elapsed) = measureTimedValue {
val note = if (noteId.startsWith("naddr")) {
val aTag = ATag.parse(noteId)
if (aTag != null) {
val note = if (noteId.contains(":")) {
val aTag = ATag.parse(noteId, null)
if (aTag != null)
LocalCache.getOrCreateAddressableNote(aTag)
} else {
else
return emptySet()
}
} else {
LocalCache.getOrCreateNote(noteId)
}
if (note.event != null) {
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
}
@@ -11,7 +11,7 @@ class Nip19 {
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? {
try {
@@ -32,36 +32,46 @@ class Nip19 {
return naddr(bytes)
}
} catch (e: Throwable) {
println("Issue trying to Decode NIP19 $uri: ${e.message}")
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
}
return null
}
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 {
return Return(Type.NOTE, bytes.toHexKey())
return Return(Type.NOTE, bytes.toHexKey(), null);
}
private fun nprofile(bytes: ByteArray): Return? {
val hex = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
val tlv = parseTLV(bytes)
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.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? {
val hex = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
val tlv = parseTLV(bytes)
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.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? {
@@ -70,7 +80,7 @@ class Nip19 {
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
return Return(Type.RELAY, relayUrl)
return Return(Type.RELAY, relayUrl, null)
}
private fun naddr(bytes: ByteArray): Return? {
@@ -92,7 +102,7 @@ class Nip19 {
?.get(0)
?.let { toInt32(it) }
return Return(Type.ADDRESS, "$kind:$author:$d")
return Return(Type.ADDRESS, "$kind:$author:$d", relay)
}
}
@@ -4,14 +4,14 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
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.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.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") {
var user: User? = null
@@ -84,7 +84,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(BadgeProfilesEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)),
authors = listOf(it.pubkeyHex),
limit = 1
)
)
@@ -11,38 +11,47 @@ import nostr.postr.Bech32
import nostr.postr.bechToBytes
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 toNAddr(): String {
val kind = kind.toByteArray()
val addr = pubKeyHex.toByteArray()
val author = pubKeyHex.toByteArray()
val dTag = dTag.toByteArray(Charsets.UTF_8)
val relay = relay?.toByteArray(Charsets.UTF_8)
val fullArray =
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag +
byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr +
var fullArray =
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag
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
return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32)
}
companion object {
fun parse(address: String): ATag? {
return if (address.startsWith("naddr") || address.startsWith("nostr:naddr")) {
parseNAddr(address)
} else {
parseAtag(address)
}
fun isATag(key: String): Boolean {
return key.startsWith("naddr1") || key.contains(":")
}
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 {
val parts = atag.split(":")
Hex.decode(parts[1])
ATag(parts[0].toInt(), parts[1], parts[2])
ATag(parts[0].toInt(), parts[1], parts[2], relay)
} catch (t: Throwable) {
Log.w("ATag", "Error parsing A Tag: $atag: ${t.message}")
Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}")
null
}
}
@@ -58,12 +67,12 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey()
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
if (kind != null && author != null) {
return ATag(kind, author, d)
}
if (kind != null && author != null)
return ATag(kind, author, d, relay)
}
} 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()
}
@@ -11,7 +11,12 @@ class BadgeAwardEvent(
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
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 {
const val kind = 8
@@ -11,7 +11,7 @@ class BadgeDefinitionEvent(
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
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 thumb() = tags.filter { it.firstOrNull() == "thumb" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
@@ -11,10 +11,15 @@ class BadgeProfilesEvent(
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
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 address() = ATag(kind, pubKey, dTag())
fun address() = ATag(kind, pubKey, dTag(), null)
companion object {
const val kind = 30008
@@ -24,8 +24,12 @@ class LnZapEvent(
override fun taggedAddresses(): List<ATag> = tags
.filter { it.firstOrNull() == "a" }
.mapNotNull { it.getOrNull(1) }
.mapNotNull { ATag.parse(it) }
.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
override fun amount(): BigDecimal? {
return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
@@ -2,8 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
import nostr.postr.Utils
import nostr.postr.toHex
class LnZapRequestEvent (
id: HexKey,
@@ -15,7 +16,12 @@ class LnZapRequestEvent(
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.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 {
const val kind = 9734
@@ -17,7 +17,7 @@ class LongTextNoteEvent(
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
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 title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
@@ -2,8 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
import nostr.postr.Utils
import nostr.postr.toHex
class ReactionEvent (
id: HexKey,
@@ -16,7 +17,12 @@ class ReactionEvent(
fun originalPost() = tags.filter { it.firstOrNull() == "e" }.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 {
const val kind = 7
@@ -2,8 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
import nostr.postr.Utils
import nostr.postr.toHex
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
@@ -47,7 +48,12 @@ class ReportEvent(
)
}
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 {
const val kind = 1984
@@ -89,6 +95,6 @@ class ReportEvent(
SPAM,
IMPERSONATION,
NUDITY,
PROFANITY
PROFANITY,
}
}
@@ -3,8 +3,8 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils
import java.util.Date
import nostr.postr.Utils
class RepostEvent (
id: HexKey,
@@ -15,9 +15,15 @@ class RepostEvent(
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.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 {
fromJson(content, Client.lenient)
@@ -14,7 +14,13 @@ class TextNoteEvent(
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
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) }
companion object {
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -27,11 +28,28 @@ fun ClickableRoute(
val text = user.toBestDisplayName()
ClickableText(
text = AnnotatedString("@$text "),
text = AnnotatedString("@${text} "),
onClick = { navController.navigate(route) },
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 {
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 noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return
@@ -55,5 +73,9 @@ fun ClickableRoute(
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.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
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.TextDirection
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.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.model.LocalCache
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.screen.loggedIn.AccountViewModel
import java.net.MalformedURLException
@@ -71,8 +79,9 @@ fun RichTextViewer(
tags: List<List<String>>?,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
navController: NavController,
) {
val myMarkDownStyle = RichTextStyle().resolveDefaults().copy(
codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy(
textStyle = TextStyle(
@@ -104,25 +113,27 @@ fun RichTextViewer(
)
Column(modifier = modifier.animateContentSize()) {
if (content.startsWith("# ") ||
content.contains("##") ||
content.contains("**") ||
content.contains("__") ||
content.contains("```")
if ( content.startsWith("# ")
|| content.contains("##")
|| content.contains("**")
|| content.contains("__")
|| content.contains("```")
) {
MaterialRichText(
style = myMarkDownStyle
style = myMarkDownStyle,
) {
Markdown(
content = content,
markdownParseOptions = MarkdownParseOptions.Default
markdownParseOptions = MarkdownParseOptions.Default,
)
}
} else {
// FlowRow doesn't work well with paragraphs. So we need to split them
content.split('\n').forEach { paragraph ->
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 ->
if (canPreview) {
// Explicit URL
@@ -151,7 +162,7 @@ fun RichTextViewer(
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
}
} else {
@@ -170,7 +181,7 @@ fun RichTextViewer(
} else {
Text(
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' }
}
fun isBechLink(word: String): Boolean {
return word.startsWith("nostr:", true) ||
word.startsWith("npub1", true) ||
word.startsWith("note1", true) ||
word.startsWith("nprofile1", true) ||
word.startsWith("nevent1", true) ||
word.startsWith("@npub1", true) ||
word.startsWith("@note1", true) ||
word.startsWith("@nprofile1", true) ||
word.startsWith("@nevent1", true)
return word.startsWith("nostr:", true)
|| word.startsWith("npub1", true)
|| word.startsWith("naddr1", true)
|| word.startsWith("note1", true)
|| word.startsWith("nprofile1", true)
|| word.startsWith("nevent1", true)
|| word.startsWith("@npub1", true)
|| word.startsWith("@note1", true)
|| word.startsWith("@addr1", true)
|| word.startsWith("@nprofile1", true)
|| word.startsWith("@nevent1", true)
}
@Composable
@@ -204,7 +218,7 @@ fun BechLink(word: String, navController: NavController) {
} else if (word.startsWith("@")) {
word.replaceFirst("@", "nostr:")
} else {
"nostr:$word"
"nostr:${word}"
}
val nip19Route = try {
@@ -220,6 +234,7 @@ fun BechLink(word: String, navController: NavController) {
}
}
@Composable
fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) {
val matcher = tagIndex.matcher(word)
@@ -228,7 +243,7 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
matcher.find()
matcher.group(1).toInt()
} catch (e: Exception) {
println("Couldn't link tag $word")
println("Couldn't link tag ${word}")
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
Text(text = "$word ")
}
} else {
} else
Text(text = "$word ")
}
}
}
@@ -18,15 +18,39 @@ class NIP19ParserTest {
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
fun nAddrFormatter() {
val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "")
val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "", null)
assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr())
}
@Test
fun nAddrFormatter2() {
val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard")
val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard", null)
assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr())
}
@Test
fun nAddrFormatter3() {
val address = ATag(30023, "d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", "89de7920", "wss://relay.damus.io")
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr())
}
}