mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-10 09:42:58 +02:00
Merge branch 'main' into add-ktlint
Conflicts: app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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() {
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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 }
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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
|
||||||
|
@@ -11,7 +11,12 @@ class BadgeAwardEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun awardees() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun awardees() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
fun awardDefinition() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
fun awardDefinition() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
|
||||||
|
val aTagValue = it.getOrNull(1)
|
||||||
|
val relay = it.getOrNull(2)
|
||||||
|
|
||||||
|
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 8
|
const val kind = 8
|
||||||
|
@@ -11,7 +11,7 @@ class BadgeDefinitionEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
||||||
fun address() = ATag(kind, pubKey, dTag())
|
fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
|
||||||
fun name() = tags.filter { it.firstOrNull() == "name" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun name() = tags.filter { it.firstOrNull() == "name" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
fun thumb() = tags.filter { it.firstOrNull() == "thumb" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun thumb() = tags.filter { it.firstOrNull() == "thumb" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
@@ -11,10 +11,15 @@ class BadgeProfilesEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
|
||||||
|
val aTagValue = it.getOrNull(1)
|
||||||
|
val relay = it.getOrNull(2)
|
||||||
|
|
||||||
|
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||||
|
}
|
||||||
|
|
||||||
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
||||||
fun address() = ATag(kind, pubKey, dTag())
|
fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 30008
|
const val kind = 30008
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@@ -17,7 +17,7 @@ class LongTextNoteEvent(
|
|||||||
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
||||||
fun address() = ATag(kind, pubKey, dTag())
|
fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
|
||||||
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
||||||
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
@@ -2,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())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,13 @@ class TextNoteEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
|
||||||
|
val aTagValue = it.getOrNull(1)
|
||||||
|
val relay = it.getOrNull(2)
|
||||||
|
|
||||||
|
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||||
|
}
|
||||||
|
|
||||||
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
|
|||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material.LocalTextStyle
|
import androidx.compose.material.LocalTextStyle
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@@ -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} "
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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 ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user