Merge branch 'main' into add-ktlint

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

View File

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

View File

@@ -6,72 +6,71 @@ import kotlin.time.measureTimedValue
class ThreadAssembler { class ThreadAssembler {
fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? { fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? {
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
testedNotes.add(note) testedNotes.add(note)
val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1)
if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot) if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot)
val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true }
if (hasNoReplyTo != null) return hasNoReplyTo if (hasNoReplyTo != null) return hasNoReplyTo
// 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()) {
return roots[0] return roots[0]
}
return null
} }
@OptIn(ExperimentalTime::class) return null
fun findThreadFor(noteId: String): Set<Note> { }
val (result, elapsed) = measureTimedValue {
val note = if (noteId.startsWith("naddr")) {
val aTag = ATag.parse(noteId)
if (aTag != null) {
LocalCache.getOrCreateAddressableNote(aTag)
} else {
return emptySet()
}
} else {
LocalCache.getOrCreateNote(noteId)
}
if (note.event != null) { @OptIn(ExperimentalTime::class)
val thread = mutableSetOf<Note>() fun findThreadFor(noteId: String): Set<Note> {
val (result, elapsed) = measureTimedValue {
val note = if (noteId.contains(":")) {
val aTag = ATag.parse(noteId, null)
if (aTag != null)
LocalCache.getOrCreateAddressableNote(aTag)
else
return emptySet()
} else {
LocalCache.getOrCreateNote(noteId)
}
val threadRoot = searchRoot(note, thread) ?: note
loadDown(threadRoot, thread) if (note.event != null) {
val thread = mutableSetOf<Note>()
thread.toSet() val threadRoot = searchRoot(note, thread) ?: note
} else {
setOf(note)
}
}
println("Model Refresh: Thread loaded in $elapsed") loadDown(threadRoot, thread)
return result thread.toSet()
} else {
setOf(note)
}
} }
fun loadDown(note: Note, thread: MutableSet<Note>) { println("Model Refresh: Thread loaded in ${elapsed}")
if (note !in thread) {
thread.add(note)
note.replies.forEach { return result
loadDown(it, thread) }
}
} fun loadDown(note: Note, thread: MutableSet<Note>) {
if (note !in thread) {
thread.add(note)
note.replies.forEach {
loadDown(it, thread)
}
} }
}
} }

View File

@@ -7,122 +7,132 @@ import java.nio.ByteOrder
class Nip19 { class Nip19 {
enum class Type { enum class Type {
USER, NOTE, RELAY, ADDRESS USER, NOTE, RELAY, ADDRESS
}
data class Return(val type: Type, val hex: String, val relay: String?)
fun uriToRoute(uri: String?): Return? {
try {
val key = uri?.removePrefix("nostr:") ?: return null
val bytes = key.bechToBytes()
if (key.startsWith("npub")) {
return npub(bytes)
} else if (key.startsWith("note")) {
return note(bytes)
} else if (key.startsWith("nprofile")) {
return nprofile(bytes)
} else if (key.startsWith("nevent")) {
return nevent(bytes)
} else if (key.startsWith("nrelay")) {
return nrelay(bytes)
} else if (key.startsWith("naddr")) {
return naddr(bytes)
}
} catch (e: Throwable) {
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
} }
data class Return(val type: Type, val hex: String) return null
}
fun uriToRoute(uri: String?): Return? { private fun npub(bytes: ByteArray): Return {
try { return Return(Type.USER, bytes.toHexKey(), null)
val key = uri?.removePrefix("nostr:") ?: return null }
val bytes = key.bechToBytes() private fun note(bytes: ByteArray): Return {
if (key.startsWith("npub")) { return Return(Type.NOTE, bytes.toHexKey(), null);
return npub(bytes) }
} else if (key.startsWith("note")) {
return note(bytes)
} else if (key.startsWith("nprofile")) {
return nprofile(bytes)
} else if (key.startsWith("nevent")) {
return nevent(bytes)
} else if (key.startsWith("nrelay")) {
return nrelay(bytes)
} else if (key.startsWith("naddr")) {
return naddr(bytes)
}
} catch (e: Throwable) {
println("Issue trying to Decode NIP19 $uri: ${e.message}")
}
return null private fun nprofile(bytes: ByteArray): Return? {
} val tlv = parseTLV(bytes)
private fun npub(bytes: ByteArray): Return { val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
return Return(Type.USER, bytes.toHexKey()) ?.get(0)
} ?.toHexKey() ?: return null
private fun note(bytes: ByteArray): Return { val relay = tlv.get(NIP19TLVTypes.RELAY.id)
return Return(Type.NOTE, bytes.toHexKey()) ?.get(0)
} ?.toString(Charsets.UTF_8)
private fun nprofile(bytes: ByteArray): Return? { return Return(Type.USER, hex, relay)
val hex = parseTLV(bytes) }
.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toHexKey() ?: return null
return Return(Type.USER, hex) private fun nevent(bytes: ByteArray): Return? {
} val tlv = parseTLV(bytes)
private fun nevent(bytes: ByteArray): Return? { val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
val hex = parseTLV(bytes) ?.get(0)
.get(NIP19TLVTypes.SPECIAL.id) ?.toHexKey() ?: return null
?.get(0)
?.toHexKey() ?: return null
return Return(Type.USER, hex) val relay = tlv.get(NIP19TLVTypes.RELAY.id)
} ?.get(0)
?.toString(Charsets.UTF_8)
private fun nrelay(bytes: ByteArray): Return? { return Return(Type.USER, hex, relay)
val relayUrl = parseTLV(bytes) }
.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
return Return(Type.RELAY, relayUrl) private fun nrelay(bytes: ByteArray): Return? {
} val relayUrl = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
private fun naddr(bytes: ByteArray): Return? { return Return(Type.RELAY, relayUrl, null)
val tlv = parseTLV(bytes) }
val d = tlv.get(NIP19TLVTypes.SPECIAL.id) private fun naddr(bytes: ByteArray): Return? {
?.get(0) val tlv = parseTLV(bytes)
?.toString(Charsets.UTF_8) ?: return null
val relay = tlv.get(NIP19TLVTypes.RELAY.id) val d = tlv.get(NIP19TLVTypes.SPECIAL.id)
?.get(0) ?.get(0)
?.toString(Charsets.UTF_8) ?.toString(Charsets.UTF_8) ?: return null
val author = tlv.get(NIP19TLVTypes.AUTHOR.id) val relay = tlv.get(NIP19TLVTypes.RELAY.id)
?.get(0) ?.get(0)
?.toHexKey() ?.toString(Charsets.UTF_8)
val kind = tlv.get(NIP19TLVTypes.KIND.id) val author = tlv.get(NIP19TLVTypes.AUTHOR.id)
?.get(0) ?.get(0)
?.let { toInt32(it) } ?.toHexKey()
return Return(Type.ADDRESS, "$kind:$author:$d") val kind = tlv.get(NIP19TLVTypes.KIND.id)
} ?.get(0)
?.let { toInt32(it) }
return Return(Type.ADDRESS, "$kind:$author:$d", relay)
}
} }
// Classes should start with an uppercase letter in kotlin // Classes should start with an uppercase letter in kotlin
enum class NIP19TLVTypes(val id: Byte) { enum class NIP19TLVTypes(val id: Byte) {
SPECIAL(0), SPECIAL(0),
RELAY(1), RELAY(1),
AUTHOR(2), AUTHOR(2),
KIND(3); KIND(3);
} }
fun toInt32(bytes: ByteArray): Int { fun toInt32(bytes: ByteArray): Int {
require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" } require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" }
return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int
} }
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> { fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
val result = mutableMapOf<Byte, MutableList<ByteArray>>() val result = mutableMapOf<Byte, MutableList<ByteArray>>()
var rest = data var rest = data
while (rest.isNotEmpty()) { while (rest.isNotEmpty()) {
val t = rest[0] val t = rest[0]
val l = rest[1] val l = rest[1]
val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
rest = rest.sliceArray(IntRange(2 + l, rest.size - 1)) rest = rest.sliceArray(IntRange(2 + l, rest.size - 1))
if (v.size < l) continue if (v.size < l) continue
if (!result.containsKey(t)) { if (!result.containsKey(t)) {
result[t] = mutableListOf() result[t] = mutableListOf()
}
result[t]?.add(v)
} }
return result result[t]?.add(v)
}
return result
} }

View File

@@ -4,114 +4,114 @@ 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?) {
if (userId != null) { if (userId != null) {
user = LocalCache.getOrCreateUser(userId) user = LocalCache.getOrCreateUser(userId)
} else { } else {
user = null user = null
}
resetFilters()
} }
fun createUserInfoFilter() = user?.let { resetFilters()
TypedFilter( }
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOf(it.pubkeyHex),
limit = 1
)
)
}
fun createUserPostsFilter() = user?.let { fun createUserInfoFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind), kinds = listOf(MetadataEvent.kind),
authors = listOf(it.pubkeyHex), authors = listOf(it.pubkeyHex),
limit = 200 limit = 1
) )
) )
} }
fun createUserReceivedZapsFilter() = user?.let { fun createUserPostsFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(LnZapEvent.kind), kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)) authors = listOf(it.pubkeyHex),
) limit = 200
) )
} )
}
fun createFollowFilter() = user?.let { fun createUserReceivedZapsFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(ContactListEvent.kind), kinds = listOf(LnZapEvent.kind),
authors = listOf(it.pubkeyHex), tags = mapOf("p" to listOf(it.pubkeyHex))
limit = 1 )
) )
) }
}
fun createFollowersFilter() = user?.let { fun createFollowFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(ContactListEvent.kind), kinds = listOf(ContactListEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)) authors = listOf(it.pubkeyHex),
) limit = 1
) )
} )
}
fun createAcceptedAwardsFilter() = user?.let { fun createFollowersFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(BadgeProfilesEvent.kind), kinds = listOf(ContactListEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)), tags = mapOf("p" to listOf(it.pubkeyHex))
limit = 1 )
) )
) }
}
fun createReceivedAwardsFilter() = user?.let { fun createAcceptedAwardsFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(BadgeAwardEvent.kind), kinds = listOf(BadgeProfilesEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)), authors = listOf(it.pubkeyHex),
limit = 20 limit = 1
) )
) )
} }
val userInfoChannel = requestNewChannel() fun createReceivedAwardsFilter() = user?.let {
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(BadgeAwardEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 20
)
)
}
override fun updateChannelFilters() { val userInfoChannel = requestNewChannel()
userInfoChannel.typedFilters = listOfNotNull(
createUserInfoFilter(), override fun updateChannelFilters() {
createUserPostsFilter(), userInfoChannel.typedFilters = listOfNotNull(
createFollowFilter(), createUserInfoFilter(),
createFollowersFilter(), createUserPostsFilter(),
createUserReceivedZapsFilter(), createFollowFilter(),
createAcceptedAwardsFilter(), createFollowersFilter(),
createReceivedAwardsFilter() createUserReceivedZapsFilter(),
).ifEmpty { null } createAcceptedAwardsFilter(),
} createReceivedAwardsFilter()
).ifEmpty { null }
}
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,53 +6,57 @@ import com.vitorpamplona.amethyst.service.relays.Client
import java.math.BigDecimal import java.math.BigDecimal
class LnZapEvent( class LnZapEvent(
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
) : 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" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
override fun zappedAuthor() = tags override fun zappedAuthor() = tags
.filter { it.firstOrNull() == "p" } .filter { it.firstOrNull() == "p" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
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)
override fun amount(): BigDecimal? { if (aTagValue != null) ATag.parse(aTagValue, relay) else null
return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
} }
// Keeps this as a field because it's a heavier function used everywhere. override fun amount(): BigDecimal? {
val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
}
override fun containedPost(): Event? = try { // Keeps this as a field because it's a heavier function used everywhere.
description()?.let { val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
fromJson(it, Client.lenient)
} override fun containedPost(): Event? = try {
} catch (e: Exception) { description()?.let {
null fromJson(it, Client.lenient)
} }
} catch (e: Exception) {
null
}
private fun lnInvoice(): String? = tags private fun lnInvoice(): String? = tags
.filter { it.firstOrNull() == "bolt11" } .filter { it.firstOrNull() == "bolt11" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
.firstOrNull() .firstOrNull()
private fun description(): String? = tags private fun description(): String? = tags
.filter { it.firstOrNull() == "description" } .filter { it.firstOrNull() == "description" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
.firstOrNull() .firstOrNull()
companion object { companion object {
const val kind = 9735 const val kind = 9735
} }
} }

View File

@@ -2,53 +2,59 @@ 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)
companion object { if (aTagValue != null) ATag.parse(aTagValue, relay) else null
const val kind = 9734 }
fun create(originalNote: EventInterface, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { companion object {
val content = "" const val kind = 9734
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(
listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey()),
listOf("relays") + relays
)
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content) fun create(originalNote: EventInterface, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
val sig = Utils.sign(id, privateKey) val content = ""
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
} var tags = listOf(
listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey()),
listOf("relays") + relays
)
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
}
fun create(userHex: String, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { val id = generateId(pubKey, createdAt, kind, tags, content)
val content = "" val sig = Utils.sign(id, privateKey)
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
val tags = listOf(
listOf("p", userHex),
listOf("relays") + relays
)
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
} }
fun create(userHex: String, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf(
listOf("p", userHex),
listOf("relays") + relays
)
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
} }
/* /*

View File

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

View File

@@ -2,44 +2,50 @@ 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)
companion object { if (aTagValue != null) ATag.parse(aTagValue, relay) else null
const val kind = 7 }
fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { companion object {
return create("\u26A0\uFE0F", originalNote, privateKey, createdAt) const val kind = 7
}
fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
return create("+", originalNote, privateKey, createdAt) return create("\u26A0\uFE0F", originalNote, privateKey, createdAt)
}
fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()))
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
} }
fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
return create("+", originalNote, privateKey, createdAt)
}
fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf( listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()))
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
} }

View File

@@ -2,93 +2,99 @@ 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.
var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull()
if (reportType == null) { if (reportType == null) {
reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull()
} }
if (reportType == null) { if (reportType == null) {
reportType = ReportType.SPAM reportType = ReportType.SPAM
} }
return reportType return reportType
}
fun reportedPost() = tags
.filter { it.firstOrNull() == "e" && it.getOrNull(1) != null }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType()
)
} }
fun reportedPost() = tags fun reportedAuthor() = tags
.filter { it.firstOrNull() == "e" && it.getOrNull(1) != null } .filter { it.firstOrNull() == "p" && it.getOrNull(1) != null }
.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 reportedAuthor() = tags
.filter { it.firstOrNull() == "p" && it.getOrNull(1) != null }
.map {
ReportedKey(
it[1],
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) }
companion object {
const val kind = 1984
fun create(reportedPost: EventInterface, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
val content = ""
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags: List<List<String>> = listOf(reportPostTag, reportAuthorTag)
if (reportedPost is LongTextNoteEvent) {
tags = tags + listOf(listOf("a", reportedPost.address().toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
val content = ""
val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags: List<List<String>> = listOf(reportAuthorTag)
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
} }
enum class ReportType() { fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
EXPLICIT, // Not used anymore. val aTagValue = it.getOrNull(1)
ILLEGAL, val relay = it.getOrNull(2)
SPAM,
IMPERSONATION, if (aTagValue != null) ATag.parse(aTagValue, relay) else null
NUDITY, }
PROFANITY
companion object {
const val kind = 1984
fun create(reportedPost: EventInterface, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
val content = ""
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
if (reportedPost is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", reportedPost.address().toTag()) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
val content = ""
val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags:List<List<String>> = listOf(reportAuthorTag)
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
enum class ReportType() {
EXPLICIT, // Not used anymore.
ILLEGAL,
SPAM,
IMPERSONATION,
NUDITY,
PROFANITY,
}
} }

View File

@@ -3,47 +3,53 @@ 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 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 containedPost() = try { fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fromJson(content, Client.lenient) fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
} catch (e: Exception) { fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
null val aTagValue = it.getOrNull(1)
} val relay = it.getOrNull(2)
companion object { if (aTagValue != null) ATag.parse(aTagValue, relay) else null
const val kind = 6 }
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent { fun containedPost() = try {
val content = boostedPost.toJson() fromJson(content, Client.lenient)
} catch (e: Exception) {
val replyToPost = listOf("e", boostedPost.id()) null
val replyToAuthor = listOf("p", boostedPost.pubKey()) }
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() companion object {
var tags: List<List<String>> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor)) const val kind = 6
if (boostedPost is LongTextNoteEvent) { fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent {
tags = tags + listOf(listOf("a", boostedPost.address().toTag())) val content = boostedPost.toJson()
}
val replyToPost = listOf("e", boostedPost.id())
val id = generateId(pubKey, createdAt, kind, tags, content) val replyToAuthor = listOf("p", boostedPost.pubKey())
val sig = Utils.sign(id, privateKey)
return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
} var tags:List<List<String>> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor))
if (boostedPost is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", boostedPost.address().toTag()) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
}
} }

View File

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

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@@ -14,46 +15,67 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
@Composable @Composable
fun ClickableRoute( fun ClickableRoute(
nip19: Nip19.Return, nip19: Nip19.Return,
navController: NavController navController: NavController
) { ) {
if (nip19.type == Nip19.Type.USER) { if (nip19.type == Nip19.Type.USER) {
val userBase = LocalCache.getOrCreateUser(nip19.hex) val userBase = LocalCache.getOrCreateUser(nip19.hex)
val userState by userBase.live().metadata.observeAsState() val userState by userBase.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
val route = "User/${nip19.hex}" val route = "User/${nip19.hex}"
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 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
if (note.event is ChannelCreateEvent) { ClickableText(
ClickableText( text = AnnotatedString("@${note.idDisplayNote()} "),
text = AnnotatedString("@${note.idDisplayNote()} "), onClick = { navController.navigate("Note/${nip19.hex}") },
onClick = { navController.navigate("Channel/${nip19.hex}") }, style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) )
)
} else if (note.channel() != null) {
ClickableText(
text = AnnotatedString("@${note.channel()?.toBestDisplayName()} "),
onClick = { navController.navigate("Channel/${note.channel()?.idHex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else {
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
if (note.event is ChannelCreateEvent) {
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Channel/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else if (note.channel() != null) {
ClickableText(
text = AnnotatedString("@${note.channel()?.toBestDisplayName()} "),
onClick = { navController.navigate("Channel/${note.channel()?.idHex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else {
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Note/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
}
} else {
Text(
"@${nip19.hex} "
)
}
} }

View File

@@ -8,20 +8,28 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -33,9 +41,9 @@ import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.Nip19 import com.vitorpamplona.amethyst.service.Nip19
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import java.net.MalformedURLException import java.net.MalformedURLException
@@ -53,234 +61,240 @@ val hashTagsPattern: Pattern = Pattern.compile("#([A-Za-z0-9_-]+)")
val urlPattern: Pattern = Patterns.WEB_URL val urlPattern: Pattern = Patterns.WEB_URL
fun isValidURL(url: String?): Boolean { fun isValidURL(url: String?): Boolean {
return try { return try {
URL(url).toURI() URL(url).toURI()
true true
} catch (e: MalformedURLException) { } catch (e: MalformedURLException) {
false false
} catch (e: URISyntaxException) { } catch (e: URISyntaxException) {
false false
} }
} }
@Composable @Composable
fun RichTextViewer( fun RichTextViewer(
content: String, content: String,
canPreview: Boolean, canPreview: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
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(
codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy(
textStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
),
modifier = Modifier
.padding(0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
)
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f).compositeOver(backgroundColor))
),
stringStyle = RichTextStyle().resolveDefaults().stringStyle?.copy(
linkStyle = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary
),
codeStyle = SpanStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor)
)
)
)
Column(modifier = modifier.animateContentSize()) { val myMarkDownStyle = RichTextStyle().resolveDefaults().copy(
if (content.startsWith("# ") || codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy(
content.contains("##") || textStyle = TextStyle(
content.contains("**") || fontFamily = FontFamily.Monospace,
content.contains("__") || fontSize = 14.sp
content.contains("```") ),
) { modifier = Modifier
MaterialRichText( .padding(0.dp)
style = myMarkDownStyle .fillMaxWidth()
) { .clip(shape = RoundedCornerShape(15.dp))
Markdown( .border(
content = content, 1.dp,
markdownParseOptions = MarkdownParseOptions.Default MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
) RoundedCornerShape(15.dp)
} )
} else { .background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f).compositeOver(backgroundColor))
// FlowRow doesn't work well with paragraphs. So we need to split them ),
content.split('\n').forEach { paragraph -> stringStyle = RichTextStyle().resolveDefaults().stringStyle?.copy(
FlowRow() { linkStyle = SpanStyle(
val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' ') textDecoration = TextDecoration.Underline,
s.forEach { word: String -> color = MaterialTheme.colors.primary
if (canPreview) { ),
// Explicit URL codeStyle = SpanStyle(
val lnInvoice = LnInvoiceUtil.findInvoice(word) fontFamily = FontFamily.Monospace,
if (lnInvoice != null) { fontSize = 14.sp,
InvoicePreview(lnInvoice) background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor)
} else if (isValidURL(word)) { )
val removedParamsFromUrl = word.split("?")[0].lowercase() )
if (imageExtension.matcher(removedParamsFromUrl).matches()) { )
ZoomableImageView(word)
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) { Column(modifier = modifier.animateContentSize()) {
VideoView(word)
} else { if ( content.startsWith("# ")
UrlPreview(word, word) || content.contains("##")
} || content.contains("**")
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { || content.contains("__")
ClickableEmail(word) || content.contains("```")
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { ) {
ClickablePhone(word)
} else if (noProtocolUrlValidator.matcher(word).matches()) { MaterialRichText(
UrlPreview("https://$word", word) style = myMarkDownStyle,
} else if (tagIndex.matcher(word).matches() && tags != null) { ) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController) Markdown(
} else if (isBechLink(word)) { content = content,
BechLink(word, navController) markdownParseOptions = MarkdownParseOptions.Default,
} else { )
Text( }
text = "$word ", } else {
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) // FlowRow doesn't work well with paragraphs. So we need to split them
) content.split('\n').forEach { paragraph ->
} FlowRow() {
} else { val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' ');
if (isValidURL(word)) { s.forEach { word: String ->
ClickableUrl("$word ", word) if (canPreview) {
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { // Explicit URL
ClickableEmail(word) val lnInvoice = LnInvoiceUtil.findInvoice(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { if (lnInvoice != null) {
ClickablePhone(word) InvoicePreview(lnInvoice)
} else if (noProtocolUrlValidator.matcher(word).matches()) { } else if (isValidURL(word)) {
ClickableUrl(word, "https://$word") val removedParamsFromUrl = word.split("?")[0].lowercase()
} else if (tagIndex.matcher(word).matches() && tags != null) { if (imageExtension.matcher(removedParamsFromUrl).matches()) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController) ZoomableImageView(word)
} else if (isBechLink(word)) { } else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
BechLink(word, navController) VideoView(word)
} else { } else {
Text( UrlPreview(word, word)
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
}
} }
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
ClickablePhone(word)
} else if (noProtocolUrlValidator.matcher(word).matches()) {
UrlPreview("https://$word", word)
} else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (isBechLink(word)) {
BechLink(word, navController)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
}
} else {
if (isValidURL(word)) {
ClickableUrl("$word ", word)
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
ClickablePhone(word)
} else if (noProtocolUrlValidator.matcher(word).matches()) {
ClickableUrl(word, "https://$word")
} else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (isBechLink(word)) {
BechLink(word, navController)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
}
} }
}
} }
}
} }
}
} }
private fun isArabic(text: String): Boolean { 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
fun BechLink(word: String, navController: NavController) { fun BechLink(word: String, navController: NavController) {
val uri = if (word.startsWith("nostr", true)) { val uri = if (word.startsWith("nostr", true)) {
word word
} 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 {
Nip19().uriToRoute(uri) Nip19().uriToRoute(uri)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
if (nip19Route == null) { if (nip19Route == null) {
Text(text = "$word ") Text(text = "$word ")
} else { } else {
ClickableRoute(nip19Route, navController) ClickableRoute(nip19Route, 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)
val index = try { val index = try {
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
} }
if (index == null) { if (index == null) {
return Text(text = "$word ") return Text(text = "$word ")
} }
if (index >= 0 && index < tags.size) { if (index >= 0 && index < tags.size) {
if (tags[index][0] == "p") { if (tags[index][0] == "p") {
val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1]) val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1])
if (baseUser != null) { if (baseUser != null) {
val userState = baseUser.live().metadata.observeAsState() val userState = baseUser.live().metadata.observeAsState()
val user = userState.value?.user val user = userState.value?.user
if (user != null) { if (user != null) {
ClickableUserTag(user, navController) ClickableUserTag(user, navController)
} else {
Text(text = "$word ")
}
} else {
// if here the tag is not a valid Nostr Hex
Text(text = "$word ")
}
} else if (tags[index][0] == "e") {
val note = LocalCache.checkGetOrCreateNote(tags[index][1])
if (note != null) {
if (canPreview) {
NoteCompose(
baseNote = note,
accountViewModel = accountViewModel,
modifier = Modifier
.padding(0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
),
parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)
.compositeOver(backgroundColor),
isQuotedNote = true,
navController = navController
)
} else {
ClickableNoteTag(note, navController)
}
} else {
// if here the tag is not a valid Nostr Hex
Text(text = "$word ")
}
} else { } else {
Text(text = "$word ") Text(text = "$word ")
} }
} } else {
// if here the tag is not a valid Nostr Hex
Text(text = "$word ")
}
} else if (tags[index][0] == "e") {
val note = LocalCache.checkGetOrCreateNote(tags[index][1])
if (note != null) {
if (canPreview) {
NoteCompose(
baseNote = note,
accountViewModel = accountViewModel,
modifier = Modifier
.padding(0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
),
parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)
.compositeOver(backgroundColor),
isQuotedNote = true,
navController = navController
)
} else {
ClickableNoteTag(note, navController)
}
} else {
// if here the tag is not a valid Nostr Hex
Text(text = "$word ")
}
} else
Text(text = "$word ")
}
} }

View File

@@ -6,27 +6,51 @@ import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
class NIP19ParserTest { class NIP19ParserTest {
@Test @Test
fun nAddrParser() { fun nAddrParser() {
val result = Nip19().uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus") val result = Nip19().uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus")
assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex) assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex)
} }
@Test @Test
fun nAddrParser2() { fun nAddrParser2() {
val result = Nip19().uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8") val result = Nip19().uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8")
assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex) assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex)
} }
@Test @Test
fun nAddrFormatter() { fun nAddrParse3() {
val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "") val result = Nip19().uriToRoute("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38")
assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr()) assertEquals(Nip19.Type.ADDRESS, result?.type)
} assertEquals("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", result?.hex)
assertEquals("wss://relay.damus.io", result?.relay)
}
@Test @Test
fun nAddrFormatter2() { fun nAddrATagParse3() {
val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard") val address = ATag.parse("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", "wss://relay.damus.io")
assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr()) 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", "", null)
assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr())
}
@Test
fun nAddrFormatter2() {
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())
}
} }