mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-19 15:51:11 +02:00
Support for Replaceable Events (NIP-33)
This commit is contained in:
@@ -27,6 +27,8 @@ Amethyst brings the best social network to your Android phone. Just insert your
|
|||||||
- [x] URI Support (NIP-21)
|
- [x] URI Support (NIP-21)
|
||||||
- [x] Event Deletion (NIP-09: like, boost, text notes and reports)
|
- [x] Event Deletion (NIP-09: like, boost, text notes and reports)
|
||||||
- [x] Identity Verification (NIP-05)
|
- [x] Identity Verification (NIP-05)
|
||||||
|
- [x] Long-form Content (NIP-23)
|
||||||
|
- [x] Parameterized Replaceable Events (NIP-33)
|
||||||
- [ ] Local Database
|
- [ ] Local Database
|
||||||
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
|
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
|
||||||
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
|
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
|
||||||
@@ -36,7 +38,6 @@ Amethyst brings the best social network to your Android phone. Just insert your
|
|||||||
- [ ] Generic Tags (NIP-12)
|
- [ ] Generic Tags (NIP-12)
|
||||||
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
|
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
|
||||||
- [ ] Events with a Subject (NIP-14)
|
- [ ] Events with a Subject (NIP-14)
|
||||||
- [ ] Long-form Content (NIP-23)
|
|
||||||
- [ ] Online Relay Search (NIP-50)
|
- [ ] Online Relay Search (NIP-50)
|
||||||
- [ ] Workspaces
|
- [ ] Workspaces
|
||||||
- [ ] Expiration Support (NIP-40)
|
- [ ] Expiration Support (NIP-40)
|
||||||
|
@@ -290,13 +290,13 @@ class Account(
|
|||||||
|
|
||||||
val repliesToHex = replyTo?.map { it.idHex }
|
val repliesToHex = replyTo?.map { it.idHex }
|
||||||
val mentionsHex = mentions?.map { it.pubkeyHex }
|
val mentionsHex = mentions?.map { it.pubkeyHex }
|
||||||
val addressesHex = replyTo?.mapNotNull { it.address() }
|
val addresses = replyTo?.mapNotNull { it.address() }
|
||||||
|
|
||||||
val signedEvent = TextNoteEvent.create(
|
val signedEvent = TextNoteEvent.create(
|
||||||
msg = message,
|
msg = message,
|
||||||
replyTos = repliesToHex,
|
replyTos = repliesToHex,
|
||||||
mentions = mentionsHex,
|
mentions = mentionsHex,
|
||||||
addresses = addressesHex,
|
addresses = addresses,
|
||||||
privateKey = loggedIn.privKey!!
|
privateKey = loggedIn.privKey!!
|
||||||
)
|
)
|
||||||
Client.send(signedEvent)
|
Client.send(signedEvent)
|
||||||
|
@@ -51,7 +51,7 @@ class Channel(val idHex: String) {
|
|||||||
fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
|
fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
|
||||||
val important = notes.values
|
val important = notes.values
|
||||||
.filter { it.author?.let { it1 -> account.isHidden(it1) } == false }
|
.filter { it.author?.let { it1 -> account.isHidden(it1) } == false }
|
||||||
.sortedBy { it.event?.createdAt }
|
.sortedBy { it.createdAt() }
|
||||||
.reversed()
|
.reversed()
|
||||||
.take(1000)
|
.take(1000)
|
||||||
.toSet()
|
.toSet()
|
||||||
|
@@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
|
|||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ATag
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
@@ -52,6 +53,7 @@ object LocalCache {
|
|||||||
val users = ConcurrentHashMap<HexKey, User>()
|
val users = ConcurrentHashMap<HexKey, User>()
|
||||||
val notes = ConcurrentHashMap<HexKey, Note>()
|
val notes = ConcurrentHashMap<HexKey, Note>()
|
||||||
val channels = ConcurrentHashMap<HexKey, Channel>()
|
val channels = ConcurrentHashMap<HexKey, Channel>()
|
||||||
|
val addressables = ConcurrentHashMap<String, AddressableNote>()
|
||||||
|
|
||||||
fun checkGetOrCreateUser(key: String): User? {
|
fun checkGetOrCreateUser(key: String): User? {
|
||||||
return try {
|
return try {
|
||||||
@@ -111,6 +113,29 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? {
|
||||||
|
return try {
|
||||||
|
val addr = ATag.parse(key)
|
||||||
|
if (addr != null)
|
||||||
|
getOrCreateAddressableNote(addr)
|
||||||
|
else
|
||||||
|
null
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Log.e("LocalCache", "Invalid Key to create channel: $key", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getOrCreateAddressableNote(key: ATag): AddressableNote {
|
||||||
|
return addressables[key.toNAddr()] ?: run {
|
||||||
|
val answer = AddressableNote(key)
|
||||||
|
answer.author = checkGetOrCreateUser(key.pubKeyHex)
|
||||||
|
addressables.put(key.toNAddr(), answer)
|
||||||
|
answer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun consume(event: MetadataEvent) {
|
fun consume(event: MetadataEvent) {
|
||||||
// new event
|
// new event
|
||||||
@@ -159,8 +184,9 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
|
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
|
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||||
|
|
||||||
note.loadEvent(event, author, mentions, replyTo)
|
note.loadEvent(event, author, mentions, replyTo)
|
||||||
|
|
||||||
@@ -193,7 +219,7 @@ object LocalCache {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
|
|
||||||
if (relay != null) {
|
if (relay != null) {
|
||||||
@@ -202,27 +228,26 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event?.id?.toHex() == event.id.toHex()) return
|
||||||
|
|
||||||
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
|
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
note.loadEvent(event, author, mentions, replyTo)
|
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||||
|
note.loadEvent(event, author, mentions, replyTo)
|
||||||
|
|
||||||
//Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content?.take(100)} ${formattedDateTime(event.createdAt)}")
|
author.addNote(note)
|
||||||
|
|
||||||
// Prepares user's profile view.
|
// Adds notifications to users.
|
||||||
author.addLongFormNote(note)
|
mentions.forEach {
|
||||||
|
it.addTaggedPost(note)
|
||||||
|
}
|
||||||
|
replyTo.forEach {
|
||||||
|
it.author?.addTaggedPost(note)
|
||||||
|
}
|
||||||
|
|
||||||
// Adds notifications to users.
|
refreshObservers()
|
||||||
mentions.forEach {
|
|
||||||
it.addTaggedPost(note)
|
|
||||||
}
|
}
|
||||||
replyTo.forEach {
|
|
||||||
it.author?.addTaggedPost(note)
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshObservers()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findCitations(event: Event): Set<String> {
|
private fun findCitations(event: Event): Set<String> {
|
||||||
@@ -245,13 +270,13 @@ object LocalCache {
|
|||||||
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> {
|
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> {
|
||||||
val citations = findCitations(event)
|
val citations = findCitations(event)
|
||||||
|
|
||||||
return event.replyTos.filter { it !in citations }
|
return event.replyTos().filter { it !in citations }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun replyToWithoutCitations(event: LongTextNoteEvent): List<String> {
|
private fun replyToWithoutCitations(event: LongTextNoteEvent): List<String> {
|
||||||
val citations = findCitations(event)
|
val citations = findCitations(event)
|
||||||
|
|
||||||
return event.replyTos.filter { it !in citations }
|
return event.replyTos().filter { it !in citations }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: RecommendRelayEvent) {
|
fun consume(event: RecommendRelayEvent) {
|
||||||
@@ -378,8 +403,9 @@ object LocalCache {
|
|||||||
//Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
//Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val repliesTo = event.boostedPost.mapNotNull { checkGetOrCreateNote(it) }
|
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
|
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||||
|
|
||||||
note.loadEvent(event, author, mentions, repliesTo)
|
note.loadEvent(event, author, mentions, repliesTo)
|
||||||
|
|
||||||
@@ -409,8 +435,9 @@ object LocalCache {
|
|||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val repliesTo = event.originalPost.mapNotNull { checkGetOrCreateNote(it) }
|
val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
|
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||||
|
|
||||||
note.loadEvent(event, author, mentions, repliesTo)
|
note.loadEvent(event, author, mentions, repliesTo)
|
||||||
|
|
||||||
@@ -459,8 +486,9 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it.key) }
|
val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) }
|
||||||
val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it.key) }
|
val repliesTo = event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } +
|
||||||
|
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||||
|
|
||||||
note.loadEvent(event, author, mentions, repliesTo)
|
note.loadEvent(event, author, mentions, repliesTo)
|
||||||
|
|
||||||
@@ -483,7 +511,7 @@ object LocalCache {
|
|||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
||||||
if (oldChannel.creator == null || oldChannel.creator == author) {
|
if (oldChannel.creator == null || oldChannel.creator == author) {
|
||||||
oldChannel.updateChannelInfo(author, event.channelInfo, event.createdAt)
|
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id.toHex())
|
||||||
oldChannel.addNote(note)
|
oldChannel.addNote(note)
|
||||||
@@ -496,15 +524,16 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun consume(event: ChannelMetadataEvent) {
|
fun consume(event: ChannelMetadataEvent) {
|
||||||
|
val channelId = event.channel()
|
||||||
//Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
|
//Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
|
||||||
if (event.channel.isNullOrBlank()) return
|
if (channelId.isNullOrBlank()) return
|
||||||
|
|
||||||
// new event
|
// new event
|
||||||
val oldChannel = checkGetOrCreateChannel(event.channel) ?: return
|
val oldChannel = checkGetOrCreateChannel(channelId) ?: return
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
||||||
if (oldChannel.creator == null || oldChannel.creator == author) {
|
if (oldChannel.creator == null || oldChannel.creator == author) {
|
||||||
oldChannel.updateChannelInfo(author, event.channelInfo, event.createdAt)
|
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id.toHex())
|
||||||
oldChannel.addNote(note)
|
oldChannel.addNote(note)
|
||||||
@@ -518,7 +547,9 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: ChannelMessageEvent, relay: Relay?) {
|
fun consume(event: ChannelMessageEvent, relay: Relay?) {
|
||||||
if (event.channel.isNullOrBlank()) return
|
val channelId = event.channel()
|
||||||
|
|
||||||
|
if (channelId.isNullOrBlank()) return
|
||||||
if (antiSpam.isSpam(event)) {
|
if (antiSpam.isSpam(event)) {
|
||||||
relay?.let {
|
relay?.let {
|
||||||
it.spamCounter++
|
it.spamCounter++
|
||||||
@@ -526,7 +557,7 @@ object LocalCache {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val channel = checkGetOrCreateChannel(event.channel) ?: return
|
val channel = checkGetOrCreateChannel(channelId) ?: return
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id.toHex())
|
||||||
channel.addNote(note)
|
channel.addNote(note)
|
||||||
@@ -541,8 +572,8 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val replyTo = event.replyTos
|
val replyTo = event.replyTos()
|
||||||
.mapNotNull { checkGetOrCreateNote(it) }
|
.mapNotNull { checkGetOrCreateNote(it) }
|
||||||
.filter { it.event !is ChannelCreateEvent }
|
.filter { it.event !is ChannelCreateEvent }
|
||||||
|
|
||||||
@@ -580,13 +611,16 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val zapRequest = event.containedPost()?.id?.toHexKey()?.let { getOrCreateNote(it) }
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) }
|
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
|
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
|
||||||
|
((zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>())
|
||||||
|
|
||||||
note.loadEvent(event, author, mentions, repliesTo)
|
note.loadEvent(event, author, mentions, repliesTo)
|
||||||
|
|
||||||
val zapRequest = event.containedPost?.id?.toHexKey()?.let { getOrCreateNote(it) }
|
|
||||||
if (zapRequest == null) {
|
if (zapRequest == null) {
|
||||||
Log.e("ZP","Zap Request not found. Unable to process Zap {${event.toJson()}}")
|
Log.e("ZP","Zap Request not found. Unable to process Zap {${event.toJson()}}")
|
||||||
return
|
return
|
||||||
@@ -617,8 +651,9 @@ object LocalCache {
|
|||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) }
|
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
|
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||||
|
|
||||||
note.loadEvent(event, author, mentions, repliesTo)
|
note.loadEvent(event, author, mentions, repliesTo)
|
||||||
|
|
||||||
@@ -652,9 +687,13 @@ object LocalCache {
|
|||||||
return notes.values.filter {
|
return notes.values.filter {
|
||||||
(it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false)
|
(it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false)
|
||||||
|| (it.event is ChannelMessageEvent && it.event?.content?.contains(text, true) ?: false)
|
|| (it.event is ChannelMessageEvent && it.event?.content?.contains(text, true) ?: false)
|
||||||
|| (it.event is LongTextNoteEvent && it.event?.content?.contains(text, true) ?: false)
|
|
||||||
|| it.idHex.startsWith(text, true)
|
|| it.idHex.startsWith(text, true)
|
||||||
|| it.idNote().startsWith(text, true)
|
|| it.idNote().startsWith(text, true)
|
||||||
|
} + addressables.values.filter {
|
||||||
|
(it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false
|
||||||
|
|| (it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false
|
||||||
|
|| (it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false
|
||||||
|
|| it.idHex.startsWith(text, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,7 +777,7 @@ object LocalCache {
|
|||||||
|
|
||||||
fun pruneHiddenMessages(account: Account) {
|
fun pruneHiddenMessages(account: Account) {
|
||||||
val toBeRemoved = account.hiddenUsers.map {
|
val toBeRemoved = account.hiddenUsers.map {
|
||||||
(users[it]?.notes ?: emptySet()) + (users[it]?.longFormNotes?.values?.flatten() ?: emptySet())
|
(users[it]?.notes ?: emptySet())
|
||||||
}.flatten()
|
}.flatten()
|
||||||
|
|
||||||
account.hiddenUsers.forEach {
|
account.hiddenUsers.forEach {
|
||||||
@@ -747,7 +786,6 @@ object LocalCache {
|
|||||||
|
|
||||||
toBeRemoved.forEach {
|
toBeRemoved.forEach {
|
||||||
it.author?.removeNote(it)
|
it.author?.removeNote(it)
|
||||||
it.author?.removeLongFormNote(it)
|
|
||||||
|
|
||||||
// reverts the add
|
// reverts the add
|
||||||
it.mentions?.forEach { user ->
|
it.mentions?.forEach { user ->
|
||||||
|
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.model
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ATag
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
@@ -27,11 +28,18 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nostr.postr.events.Event
|
import nostr.postr.events.Event
|
||||||
import nostr.postr.toHex
|
|
||||||
|
|
||||||
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
|
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
|
||||||
|
|
||||||
class Note(val idHex: String) {
|
|
||||||
|
class AddressableNote(val address: ATag): Note(address.toNAddr()) {
|
||||||
|
override fun idNote() = address.toNAddr()
|
||||||
|
override fun idDisplayNote() = idNote().toShortenHex()
|
||||||
|
override fun address() = address
|
||||||
|
override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Note(val idHex: String) {
|
||||||
// These fields are only available after the Text Note event is received.
|
// These fields are only available after the Text Note event is received.
|
||||||
// They are immutable after that.
|
// They are immutable after that.
|
||||||
var event: Event? = null
|
var event: Event? = null
|
||||||
@@ -57,18 +65,21 @@ class Note(val idHex: String) {
|
|||||||
var lastReactionsDownloadTime: Long? = null
|
var lastReactionsDownloadTime: Long? = null
|
||||||
|
|
||||||
fun id() = Hex.decode(idHex)
|
fun id() = Hex.decode(idHex)
|
||||||
fun idNote() = id().toNote()
|
open fun idNote() = id().toNote()
|
||||||
fun idDisplayNote() = idNote().toShortenHex()
|
open fun idDisplayNote() = idNote().toShortenHex()
|
||||||
|
|
||||||
fun channel(): Channel? {
|
fun channel(): Channel? {
|
||||||
val channelHex = (event as? ChannelMessageEvent)?.channel ?:
|
val channelHex =
|
||||||
(event as? ChannelMetadataEvent)?.channel ?:
|
(event as? ChannelMessageEvent)?.channel() ?:
|
||||||
(event as? ChannelCreateEvent)?.let { idHex }
|
(event as? ChannelMetadataEvent)?.channel() ?:
|
||||||
|
(event as? ChannelCreateEvent)?.let { it.id.toHexKey() }
|
||||||
|
|
||||||
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
|
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun address() = (event as? LongTextNoteEvent)?.address
|
open fun address() = (event as? LongTextNoteEvent)?.address()
|
||||||
|
|
||||||
|
open fun createdAt() = event?.createdAt
|
||||||
|
|
||||||
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: List<Note>) {
|
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: List<Note>) {
|
||||||
this.event = event
|
this.event = event
|
||||||
@@ -90,14 +101,14 @@ class Note(val idHex: String) {
|
|||||||
fun replyLevelSignature(cachedSignatures: MutableMap<Note, String> = mutableMapOf()): String {
|
fun replyLevelSignature(cachedSignatures: MutableMap<Note, String> = mutableMapOf()): String {
|
||||||
val replyTo = replyTo
|
val replyTo = replyTo
|
||||||
if (replyTo == null || replyTo.isEmpty()) {
|
if (replyTo == null || replyTo.isEmpty()) {
|
||||||
return "/" + formattedDateTime(event?.createdAt ?: 0) + ";"
|
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
||||||
}
|
}
|
||||||
|
|
||||||
return replyTo
|
return replyTo
|
||||||
.map {
|
.map {
|
||||||
cachedSignatures[it] ?: it.replyLevelSignature(cachedSignatures).apply { cachedSignatures.put(it, this) }
|
cachedSignatures[it] ?: it.replyLevelSignature(cachedSignatures).apply { cachedSignatures.put(it, this) }
|
||||||
}
|
}
|
||||||
.maxBy { it.length }.removeSuffix(";") + "/" + formattedDateTime(event?.createdAt ?: 0) + ";"
|
.maxBy { it.length }.removeSuffix(";") + "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replyLevel(cachedLevels: MutableMap<Note, Int> = mutableMapOf()): Int {
|
fun replyLevel(cachedLevels: MutableMap<Note, Int> = mutableMapOf()): Int {
|
||||||
@@ -236,7 +247,7 @@ class Note(val idHex: String) {
|
|||||||
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.event?.createdAt ?: 0 ) > dayAgo } != null
|
it.firstOrNull { ( it.createdAt() ?: 0 ) > dayAgo } != null
|
||||||
}?.isNotEmpty() ?: false)
|
}?.isNotEmpty() ?: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +294,7 @@ 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.event?.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> {
|
||||||
@@ -356,12 +367,21 @@ class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) {
|
|||||||
|
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
super.onActive()
|
super.onActive()
|
||||||
NostrSingleEventDataSource.add(note.idHex)
|
if (note is AddressableNote) {
|
||||||
|
NostrSingleEventDataSource.addAddress(note)
|
||||||
|
} else {
|
||||||
|
NostrSingleEventDataSource.add(note)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInactive() {
|
override fun onInactive() {
|
||||||
super.onInactive()
|
super.onInactive()
|
||||||
NostrSingleEventDataSource.remove(note.idHex)
|
if (note is AddressableNote) {
|
||||||
|
NostrSingleEventDataSource.removeAddress(note)
|
||||||
|
} else {
|
||||||
|
NostrSingleEventDataSource.remove(note)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ATag
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
import kotlin.time.measureTimedValue
|
import kotlin.time.measureTimedValue
|
||||||
|
|
||||||
@@ -34,7 +35,16 @@ class ThreadAssembler {
|
|||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
fun findThreadFor(noteId: String): Set<Note> {
|
fun findThreadFor(noteId: String): Set<Note> {
|
||||||
val (result, elapsed) = measureTimedValue {
|
val (result, elapsed) = measureTimedValue {
|
||||||
val note = LocalCache.getOrCreateNote(noteId)
|
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) {
|
if (note.event != null) {
|
||||||
val thread = mutableSetOf<Note>()
|
val thread = mutableSetOf<Note>()
|
||||||
|
@@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.model
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||||
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.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
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
|
||||||
@@ -38,8 +37,7 @@ class User(val pubkeyHex: String) {
|
|||||||
|
|
||||||
var notes = setOf<Note>()
|
var notes = setOf<Note>()
|
||||||
private set
|
private set
|
||||||
var longFormNotes = mapOf<String, Set<Note>>()
|
|
||||||
private set
|
|
||||||
var taggedPosts = setOf<Note>()
|
var taggedPosts = setOf<Note>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -145,27 +143,8 @@ class User(val pubkeyHex: String) {
|
|||||||
notes = notes - note
|
notes = notes - note
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addLongFormNote(note: Note) {
|
|
||||||
val address = (note.event as LongTextNoteEvent).address
|
|
||||||
|
|
||||||
if (address in longFormNotes.keys) {
|
|
||||||
if (longFormNotes[address]?.contains(note) == false)
|
|
||||||
longFormNotes = longFormNotes + Pair(address, (longFormNotes[address] ?: emptySet()) + note)
|
|
||||||
} else {
|
|
||||||
longFormNotes = longFormNotes + Pair(address, setOf(note))
|
|
||||||
// No need for Listener yet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeLongFormNote(note: Note) {
|
|
||||||
val address = (note.event as LongTextNoteEvent).address ?: return
|
|
||||||
|
|
||||||
longFormNotes = longFormNotes - address
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearNotes() {
|
fun clearNotes() {
|
||||||
notes = setOf<Note>()
|
notes = setOf<Note>()
|
||||||
longFormNotes = mapOf<String, Set<Note>>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addReport(note: Note) {
|
fun addReport(note: Note) {
|
||||||
@@ -179,7 +158,7 @@ class User(val pubkeyHex: String) {
|
|||||||
liveSet?.reports?.invalidateData()
|
liveSet?.reports?.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
val reportTime = note.event?.createdAt ?: 0
|
val reportTime = note.createdAt() ?: 0
|
||||||
if (reportTime > latestReportTime) {
|
if (reportTime > latestReportTime) {
|
||||||
latestReportTime = reportTime
|
latestReportTime = reportTime
|
||||||
}
|
}
|
||||||
@@ -311,7 +290,7 @@ class User(val pubkeyHex: String) {
|
|||||||
|
|
||||||
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
|
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
|
||||||
return reports[loggedIn]?.firstOrNull() {
|
return reports[loggedIn]?.firstOrNull() {
|
||||||
it.event is ReportEvent && (it.event as ReportEvent).reportedAuthor.any { it.reportType == type }
|
it.event is ReportEvent && (it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
|
||||||
} != null
|
} != null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +343,7 @@ data class RelayInfo (
|
|||||||
|
|
||||||
data class Chatroom(var roomMessages: Set<Note>)
|
data class Chatroom(var roomMessages: Set<Note>)
|
||||||
|
|
||||||
|
|
||||||
class UserMetadata {
|
class UserMetadata {
|
||||||
var name: String? = null
|
var name: String? = null
|
||||||
var username: String? = null
|
var username: String? = null
|
||||||
|
@@ -1,12 +1,18 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ATag
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import nostr.postr.Bech32
|
||||||
import nostr.postr.bechToBytes
|
import nostr.postr.bechToBytes
|
||||||
|
import nostr.postr.toByteArray
|
||||||
|
|
||||||
class Nip19 {
|
class Nip19 {
|
||||||
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
USER, NOTE
|
USER, NOTE, RELAY, ADDRESS
|
||||||
}
|
}
|
||||||
data class Return(val type: Type, val hex: String)
|
data class Return(val type: Type, val hex: String)
|
||||||
|
|
||||||
@@ -24,16 +30,31 @@ class Nip19 {
|
|||||||
}
|
}
|
||||||
if (key.startsWith("nprofile")) {
|
if (key.startsWith("nprofile")) {
|
||||||
val tlv = parseTLV(bytes)
|
val tlv = parseTLV(bytes)
|
||||||
val hex = tlv.get(0)?.get(0)?.toHexKey()
|
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey()
|
||||||
if (hex != null)
|
if (hex != null)
|
||||||
return Return(Type.USER, hex)
|
return Return(Type.USER, hex)
|
||||||
}
|
}
|
||||||
if (key.startsWith("nevent")) {
|
if (key.startsWith("nevent")) {
|
||||||
val tlv = parseTLV(bytes)
|
val tlv = parseTLV(bytes)
|
||||||
val hex = tlv.get(0)?.get(0)?.toHexKey()
|
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey()
|
||||||
if (hex != null)
|
if (hex != null)
|
||||||
return Return(Type.USER, hex)
|
return Return(Type.USER, hex)
|
||||||
}
|
}
|
||||||
|
if (key.startsWith("nrelay")) {
|
||||||
|
val tlv = parseTLV(bytes)
|
||||||
|
val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8)
|
||||||
|
if (relayUrl != null)
|
||||||
|
return Return(Type.RELAY, relayUrl)
|
||||||
|
}
|
||||||
|
if (key.startsWith("naddr")) {
|
||||||
|
val tlv = parseTLV(bytes)
|
||||||
|
val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8)
|
||||||
|
val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8)
|
||||||
|
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey()
|
||||||
|
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
|
||||||
|
if (d != null)
|
||||||
|
return Return(Type.ADDRESS, "$kind:$author:$d")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
|
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
|
||||||
@@ -42,22 +63,34 @@ class Nip19 {
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
|
enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin
|
||||||
var result = mutableMapOf<Byte, MutableList<ByteArray>>()
|
SPECIAL(0),
|
||||||
var rest = data
|
RELAY(1),
|
||||||
while (rest.isNotEmpty()) {
|
AUTHOR(2),
|
||||||
val t = rest[0]
|
KIND(3);
|
||||||
val l = rest[1]
|
}
|
||||||
val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
|
|
||||||
rest = rest.sliceArray(IntRange(2 + l, rest.size-1))
|
|
||||||
if (v.size < l) continue
|
|
||||||
|
|
||||||
if (!result.containsKey(t)) {
|
fun toInt32(bytes: ByteArray): Int {
|
||||||
result.put(t, mutableListOf())
|
require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" }
|
||||||
}
|
return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int
|
||||||
result.get(t)?.add(v)
|
}
|
||||||
|
|
||||||
|
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
|
||||||
|
var result = mutableMapOf<Byte, MutableList<ByteArray>>()
|
||||||
|
var rest = data
|
||||||
|
while (rest.isNotEmpty()) {
|
||||||
|
val t = rest[0]
|
||||||
|
val l = rest[1]
|
||||||
|
val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
|
||||||
|
rest = rest.sliceArray(IntRange(2 + l, rest.size-1))
|
||||||
|
if (v.size < l) continue
|
||||||
|
|
||||||
|
if (!result.containsKey(t)) {
|
||||||
|
result.put(t, mutableListOf())
|
||||||
}
|
}
|
||||||
return result
|
result.get(t)?.add(v)
|
||||||
}
|
}
|
||||||
}
|
return result
|
||||||
|
}
|
||||||
|
@@ -74,7 +74,7 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
RepostEvent.kind -> {
|
RepostEvent.kind -> {
|
||||||
val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
|
val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
|
||||||
|
|
||||||
repostEvent.containedPost?.let { onEvent(it, subscriptionId, relay) }
|
repostEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) }
|
||||||
LocalCache.consume(repostEvent)
|
LocalCache.consume(repostEvent)
|
||||||
}
|
}
|
||||||
ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
||||||
@@ -83,7 +83,7 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
LnZapEvent.kind -> {
|
LnZapEvent.kind -> {
|
||||||
val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
|
val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
|
||||||
|
|
||||||
zapEvent.containedPost?.let { onEvent(it, subscriptionId, relay) }
|
zapEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) }
|
||||||
LocalCache.consume(zapEvent)
|
LocalCache.consume(zapEvent)
|
||||||
}
|
}
|
||||||
LnZapRequestEvent.kind -> LocalCache.consume(LnZapRequestEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
LnZapRequestEvent.kind -> LocalCache.consume(LnZapRequestEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
@@ -17,10 +17,11 @@ import nostr.postr.JsonFilter
|
|||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
||||||
private var eventsToWatch = setOf<String>()
|
private var eventsToWatch = setOf<Note>()
|
||||||
|
private var addressesToWatch = setOf<Note>()
|
||||||
|
|
||||||
private fun createAddressFilter(): List<TypedFilter>? {
|
private fun createAddressFilter(): List<TypedFilter>? {
|
||||||
val addressesToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }.filter { it.address() != null }
|
val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch
|
||||||
|
|
||||||
if (addressesToWatch.isEmpty()) {
|
if (addressesToWatch.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
@@ -31,22 +32,24 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
return addressesToWatch.filter {
|
return addressesToWatch.filter {
|
||||||
val lastTime = it.lastReactionsDownloadTime
|
val lastTime = it.lastReactionsDownloadTime
|
||||||
lastTime == null || lastTime < (now - 10)
|
lastTime == null || lastTime < (now - 10)
|
||||||
}.map {
|
}.mapNotNull {
|
||||||
TypedFilter(
|
it.address()?.let { aTag ->
|
||||||
types = FeedType.values().toSet(),
|
TypedFilter(
|
||||||
filter = JsonFilter(
|
types = FeedType.values().toSet(),
|
||||||
kinds = listOf(
|
filter = JsonFilter(
|
||||||
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind
|
kinds = listOf(
|
||||||
),
|
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind
|
||||||
tags = mapOf("a" to listOf(it.address()!!)),
|
),
|
||||||
since = it.lastReactionsDownloadTime
|
tags = mapOf("a" to listOf(aTag.toTag())),
|
||||||
|
since = it.lastReactionsDownloadTime
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
|
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
|
||||||
val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }
|
val reactionsToWatch = eventsToWatch
|
||||||
|
|
||||||
if (reactionsToWatch.isEmpty()) {
|
if (reactionsToWatch.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
@@ -73,11 +76,9 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
|
|
||||||
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
|
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
|
||||||
val directEventsToLoad = eventsToWatch
|
val directEventsToLoad = eventsToWatch
|
||||||
.map { LocalCache.getOrCreateNote(it) }
|
|
||||||
.filter { it.event == null }
|
.filter { it.event == null }
|
||||||
|
|
||||||
val threadingEventsToLoad = eventsToWatch
|
val threadingEventsToLoad = eventsToWatch
|
||||||
.map { LocalCache.getOrCreateNote(it) }
|
|
||||||
.mapNotNull { it.replyTo }
|
.mapNotNull { it.replyTo }
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter { it.event == null }
|
.filter { it.event == null }
|
||||||
@@ -107,7 +108,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
|
|
||||||
val singleEventChannel = requestNewChannel { time ->
|
val singleEventChannel = requestNewChannel { time ->
|
||||||
eventsToWatch.forEach {
|
eventsToWatch.forEach {
|
||||||
LocalCache.getOrCreateNote(it).lastReactionsDownloadTime = time
|
it.lastReactionsDownloadTime = time
|
||||||
}
|
}
|
||||||
// Many relays operate with limits in the amount of filters.
|
// Many relays operate with limits in the amount of filters.
|
||||||
// As information comes, the filters will be rotated to get more data.
|
// As information comes, the filters will be rotated to get more data.
|
||||||
@@ -122,13 +123,23 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses).flatten().ifEmpty { null }
|
singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses).flatten().ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(eventId: String) {
|
fun add(eventId: Note) {
|
||||||
eventsToWatch = eventsToWatch.plus(eventId)
|
eventsToWatch = eventsToWatch.plus(eventId)
|
||||||
invalidateFilters()
|
invalidateFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(eventId: String) {
|
fun remove(eventId: Note) {
|
||||||
eventsToWatch = eventsToWatch.minus(eventId)
|
eventsToWatch = eventsToWatch.minus(eventId)
|
||||||
invalidateFilters()
|
invalidateFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addAddress(aTag: Note) {
|
||||||
|
addressesToWatch = addressesToWatch.plus(aTag)
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAddress(aTag: Note) {
|
||||||
|
addressesToWatch = addressesToWatch.minus(aTag)
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,72 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import com.vitorpamplona.amethyst.service.NIP19TLVTypes
|
||||||
|
import com.vitorpamplona.amethyst.service.parseTLV
|
||||||
|
import com.vitorpamplona.amethyst.service.toInt32
|
||||||
|
import fr.acinq.secp256k1.Hex
|
||||||
|
import nostr.postr.Bech32
|
||||||
|
import nostr.postr.bechToBytes
|
||||||
|
import nostr.postr.toByteArray
|
||||||
|
|
||||||
|
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
|
||||||
|
fun toTag() = "$kind:$pubKeyHex:$dTag"
|
||||||
|
|
||||||
|
fun toNAddr(): String {
|
||||||
|
val kind = kind.toByteArray()
|
||||||
|
val addr = pubKeyHex.toByteArray()
|
||||||
|
val dTag = dTag.toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
|
val fullArray =
|
||||||
|
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag +
|
||||||
|
byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr +
|
||||||
|
byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind
|
||||||
|
|
||||||
|
return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(address: String): ATag? {
|
||||||
|
return if (address.startsWith("naddr") || address.startsWith("nostr:naddr"))
|
||||||
|
parseNAddr(address)
|
||||||
|
else
|
||||||
|
parseAtag(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseAtag(atag: String): ATag? {
|
||||||
|
return try {
|
||||||
|
val parts = atag.split(":")
|
||||||
|
Hex.decode(parts[1])
|
||||||
|
ATag(parts[0].toInt(), parts[1], parts[2])
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.w("Address", "Error parsing A Tag: ${atag}: ${t.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseNAddr(naddr: String): ATag? {
|
||||||
|
try {
|
||||||
|
val key = naddr.removePrefix("nostr:")
|
||||||
|
|
||||||
|
if (key.startsWith("naddr")) {
|
||||||
|
val tlv = parseTLV(key.bechToBytes())
|
||||||
|
val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: ""
|
||||||
|
val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8)
|
||||||
|
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey()
|
||||||
|
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
|
||||||
|
|
||||||
|
if (kind != null && author != null)
|
||||||
|
return ATag(kind, author, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
println("Issue trying to Decode NIP19 ${this}: ${e.message}")
|
||||||
|
//e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -14,15 +14,11 @@ class ChannelCreateEvent (
|
|||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
@Transient val channelInfo: ChannelData
|
fun channelInfo() = try {
|
||||||
|
MetadataEvent.gson.fromJson(content, ChannelData::class.java)
|
||||||
init {
|
} catch (e: Exception) {
|
||||||
channelInfo = try {
|
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
|
||||||
MetadataEvent.gson.fromJson(content, ChannelData::class.java)
|
ChannelData(null, null, null)
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
|
|
||||||
ChannelData(null, null, null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@@ -12,11 +12,7 @@ class ChannelHideMessageEvent (
|
|||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
@Transient val eventsToHide: List<String>
|
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
init {
|
|
||||||
eventsToHide = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 43
|
const val kind = 43
|
||||||
|
@@ -12,15 +12,10 @@ class ChannelMessageEvent (
|
|||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
@Transient val channel: String?
|
|
||||||
@Transient val replyTos: List<String>
|
|
||||||
@Transient val mentions: List<String>
|
|
||||||
|
|
||||||
init {
|
fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
||||||
channel = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
fun replyTos() = tags.filter { it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) }
|
||||||
replyTos = tags.filter { it.getOrNull(1) != channel }.mapNotNull { it.getOrNull(1) }
|
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 42
|
const val kind = 42
|
||||||
|
@@ -14,19 +14,14 @@ class ChannelMetadataEvent (
|
|||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
@Transient val channel: String?
|
fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
||||||
@Transient val channelInfo: ChannelCreateEvent.ChannelData
|
fun channelInfo() =
|
||||||
|
try {
|
||||||
init {
|
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
|
||||||
channel = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
} catch (e: Exception) {
|
||||||
channelInfo =
|
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
|
||||||
try {
|
ChannelCreateEvent.ChannelData(null, null, null)
|
||||||
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
|
|
||||||
ChannelCreateEvent.ChannelData(null, null, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 41
|
const val kind = 41
|
||||||
|
@@ -12,11 +12,9 @@ class ChannelMuteUserEvent (
|
|||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
@Transient val usersToMute: List<String>
|
|
||||||
|
|
||||||
init {
|
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
usersToMute = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 44
|
const val kind = 44
|
||||||
|
@@ -14,31 +14,25 @@ class LnZapEvent (
|
|||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
@Transient val zappedPost: List<String>
|
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
@Transient val zappedAuthor: List<String>
|
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
@Transient val containedPost: Event?
|
|
||||||
@Transient val lnInvoice: String?
|
|
||||||
@Transient val preimage: String?
|
|
||||||
@Transient val amount: BigDecimal?
|
|
||||||
|
|
||||||
init {
|
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||||
zappedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
zappedAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
|
|
||||||
lnInvoice = tags.filter { it.firstOrNull() == "bolt11" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun lnInvoice() = tags.filter { it.firstOrNull() == "bolt11" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
amount = lnInvoice?.let { LnInvoiceUtil.getAmountInSats(lnInvoice) }
|
fun preimage() = tags.filter { it.firstOrNull() == "preimage" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
preimage = tags.filter { it.firstOrNull() == "preimage" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
|
||||||
|
|
||||||
val description = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun description() = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
|
||||||
containedPost = try {
|
// Keeps this as a field because it's a heavier function used everywhere.
|
||||||
if (description == null)
|
val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
|
||||||
null
|
|
||||||
else
|
fun containedPost() = try {
|
||||||
fromJson(description, Client.lenient)
|
description()?.let {
|
||||||
} catch (e: Exception) {
|
fromJson(it, Client.lenient)
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@@ -13,14 +13,9 @@ class LnZapRequestEvent (
|
|||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): 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) }
|
||||||
@Transient val zappedPost: List<String>
|
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
@Transient val zappedAuthor: List<String>
|
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||||
|
|
||||||
init {
|
|
||||||
zappedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
zappedAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 9734
|
const val kind = 9734
|
||||||
@@ -34,7 +29,7 @@ class LnZapRequestEvent (
|
|||||||
listOf("relays") + relays
|
listOf("relays") + relays
|
||||||
)
|
)
|
||||||
if (originalNote is LongTextNoteEvent) {
|
if (originalNote is LongTextNoteEvent) {
|
||||||
tags = tags + listOf( listOf("a", originalNote.address) )
|
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
@@ -13,33 +13,21 @@ class LongTextNoteEvent(
|
|||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
@Transient val replyTos: List<String>
|
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
@Transient val mentions: List<String>
|
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
@Transient val title: String?
|
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
||||||
@Transient val image: String?
|
fun address() = ATag(kind, pubKey.toHexKey(), dTag())
|
||||||
@Transient val summary: String?
|
|
||||||
@Transient val publishedAt: Long?
|
|
||||||
@Transient val topics: List<String>
|
|
||||||
@Transient val address: String
|
|
||||||
@Transient val dTag: String?
|
|
||||||
|
|
||||||
init {
|
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
||||||
replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun image() = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
fun summary() = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
|
||||||
dTag = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun publishedAt() = try {
|
||||||
|
tags.filter { it.firstOrNull() == "published_at" }.mapNotNull { it.getOrNull(1) }.firstOrNull()?.toLong()
|
||||||
address = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "$kind:${pubKey.toHexKey()}:$dTag"
|
} catch (_: Exception) {
|
||||||
topics = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
null
|
||||||
title = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
|
||||||
image = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
|
||||||
summary = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
|
||||||
publishedAt = try {
|
|
||||||
tags.filter { it.firstOrNull() == "published_at" }.mapNotNull { it.getOrNull(1) }.firstOrNull()?.toLong()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -59,4 +47,4 @@ class LongTextNoteEvent(
|
|||||||
return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig)
|
return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,13 +14,9 @@ class ReactionEvent (
|
|||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
@Transient val originalPost: List<String>
|
fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
@Transient val originalAuthor: List<String>
|
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) }
|
||||||
init {
|
|
||||||
originalPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
originalAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 7
|
const val kind = 7
|
||||||
@@ -38,7 +34,7 @@ class ReactionEvent (
|
|||||||
|
|
||||||
var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex()))
|
var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex()))
|
||||||
if (originalNote is LongTextNoteEvent) {
|
if (originalNote is LongTextNoteEvent) {
|
||||||
tags = tags + listOf( listOf("a", originalNote.address) )
|
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
@@ -17,12 +17,8 @@ class ReportEvent (
|
|||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
@Transient val reportedPost: List<ReportedKey>
|
private fun defaultReportType(): ReportType {
|
||||||
@Transient val reportedAuthor: List<ReportedKey>
|
|
||||||
|
|
||||||
init {
|
|
||||||
// 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()
|
||||||
@@ -30,26 +26,29 @@ class ReportEvent (
|
|||||||
if (reportType == null) {
|
if (reportType == null) {
|
||||||
reportType = ReportType.SPAM
|
reportType = ReportType.SPAM
|
||||||
}
|
}
|
||||||
|
return reportType
|
||||||
reportedPost = tags
|
|
||||||
.filter { it.firstOrNull() == "e" && it.getOrNull(1) != null }
|
|
||||||
.map {
|
|
||||||
ReportedKey(
|
|
||||||
it[1],
|
|
||||||
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: reportType
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
reportedAuthor = tags
|
|
||||||
.filter { it.firstOrNull() == "p" && it.getOrNull(1) != null }
|
|
||||||
.map {
|
|
||||||
ReportedKey(
|
|
||||||
it[1],
|
|
||||||
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: 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 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 {
|
companion object {
|
||||||
const val kind = 1984
|
const val kind = 1984
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ class ReportEvent (
|
|||||||
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
|
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
|
||||||
|
|
||||||
if (reportedPost is LongTextNoteEvent) {
|
if (reportedPost is LongTextNoteEvent) {
|
||||||
tags = tags + listOf( listOf("a", reportedPost.address) )
|
tags = tags + listOf( listOf("a", reportedPost.address().toTag()) )
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
@@ -15,19 +15,15 @@ class RepostEvent (
|
|||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
@Transient val boostedPost: List<String>
|
|
||||||
@Transient val originalAuthor: List<String>
|
|
||||||
@Transient val containedPost: Event?
|
|
||||||
|
|
||||||
init {
|
fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
boostedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
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) }
|
||||||
|
|
||||||
containedPost = try {
|
fun containedPost() = try {
|
||||||
fromJson(content, Client.lenient)
|
fromJson(content, Client.lenient)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -43,7 +39,7 @@ class RepostEvent (
|
|||||||
var tags:List<List<String>> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor))
|
var tags:List<List<String>> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor))
|
||||||
|
|
||||||
if (boostedPost is LongTextNoteEvent) {
|
if (boostedPost is LongTextNoteEvent) {
|
||||||
tags = tags + listOf( listOf("a", boostedPost.address) )
|
tags = tags + listOf( listOf("a", boostedPost.address().toTag()) )
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
@@ -12,20 +12,14 @@ class TextNoteEvent(
|
|||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: ByteArray
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
@Transient val replyTos: List<String>
|
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
@Transient val mentions: List<String>
|
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||||
@Transient val longFormAddress: List<String>
|
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
init {
|
|
||||||
longFormAddress = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 1
|
const val kind = 1
|
||||||
|
|
||||||
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, addresses: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent {
|
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, addresses: List<ATag>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent {
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||||
val tags = mutableListOf<List<String>>()
|
val tags = mutableListOf<List<String>>()
|
||||||
replyTos?.forEach {
|
replyTos?.forEach {
|
||||||
@@ -35,7 +29,7 @@ class TextNoteEvent(
|
|||||||
tags.add(listOf("p", it))
|
tags.add(listOf("p", it))
|
||||||
}
|
}
|
||||||
addresses?.forEach {
|
addresses?.forEach {
|
||||||
tags.add(listOf("a", it))
|
tags.add(listOf("a", it.toTag()))
|
||||||
}
|
}
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
@@ -16,6 +16,6 @@ object ChannelFeedFilter: FeedFilter<Note>() {
|
|||||||
|
|
||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
return channel.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList()
|
return channel.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.createdAt() }?.reversed() ?: emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -23,6 +23,6 @@ object ChatroomFeedFilter: FeedFilter<Note>() {
|
|||||||
|
|
||||||
val messages = myAccount.userProfile().privateChatrooms[myUser] ?: return emptyList()
|
val messages = myAccount.userProfile().privateChatrooms[myUser] ?: return emptyList()
|
||||||
|
|
||||||
return messages.roomMessages.filter { myAccount.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed()
|
return messages.roomMessages.filter { myAccount.isAcceptable(it) }.sortedBy { it.createdAt() }.reversed()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -17,17 +17,17 @@ object ChatroomListKnownFeedFilter: FeedFilter<Note>() {
|
|||||||
|
|
||||||
val privateMessages = messagingWith.mapNotNull {
|
val privateMessages = messagingWith.mapNotNull {
|
||||||
privateChatrooms[it]?.roomMessages?.sortedBy {
|
privateChatrooms[it]?.roomMessages?.sortedBy {
|
||||||
it.event?.createdAt
|
it.createdAt()
|
||||||
}?.lastOrNull {
|
}?.lastOrNull {
|
||||||
it.event != null
|
it.event != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val publicChannels = account.followingChannels().map {
|
val publicChannels = account.followingChannels().map {
|
||||||
it.notes.values.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null }
|
it.notes.values.filter { account.isAcceptable(it) }.sortedBy { it.createdAt() }.lastOrNull { it.event != null }
|
||||||
}
|
}
|
||||||
|
|
||||||
return (privateMessages + publicChannels).filterNotNull().sortedBy { it.event?.createdAt }.reversed()
|
return (privateMessages + publicChannels).filterNotNull().sortedBy { it.createdAt() }.reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -17,13 +17,13 @@ object ChatroomListNewFeedFilter: FeedFilter<Note>() {
|
|||||||
|
|
||||||
val privateMessages = messagingWith.mapNotNull {
|
val privateMessages = messagingWith.mapNotNull {
|
||||||
privateChatrooms[it]?.roomMessages?.sortedBy {
|
privateChatrooms[it]?.roomMessages?.sortedBy {
|
||||||
it.event?.createdAt
|
it.createdAt()
|
||||||
}?.lastOrNull {
|
}?.lastOrNull {
|
||||||
it.event != null
|
it.event != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return privateMessages.sortedBy { it.event?.createdAt }.reversed()
|
return privateMessages.sortedBy { it.createdAt() }.reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object GlobalFeedFilter: FeedFilter<Note>() {
|
object GlobalFeedFilter: FeedFilter<Note>() {
|
||||||
@@ -11,11 +12,17 @@ object GlobalFeedFilter: FeedFilter<Note>() {
|
|||||||
|
|
||||||
override fun feed() = LocalCache.notes.values
|
override fun feed() = LocalCache.notes.values
|
||||||
.filter {
|
.filter {
|
||||||
(it.event is TextNoteEvent && (it.event as TextNoteEvent).replyTos.isEmpty()) ||
|
(it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent)
|
||||||
(it.event is ChannelMessageEvent && (it.event as ChannelMessageEvent).replyTos.isEmpty())
|
&& it.replyTo.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
.filter {
|
||||||
|
// does not show events already in the public chat list
|
||||||
|
(it.channel() == null || it.channel() !in account.followingChannels())
|
||||||
|
// does not show people the user already follows
|
||||||
|
&& (it.author !in account.userProfile().follows)
|
||||||
}
|
}
|
||||||
.filter { account.isAcceptable(it) }
|
.filter { account.isAcceptable(it) }
|
||||||
.sortedBy { it.event?.createdAt }
|
.sortedBy { it.createdAt() }
|
||||||
.reversed()
|
.reversed()
|
||||||
|
|
||||||
}
|
}
|
@@ -20,7 +20,7 @@ object HomeConversationsFeedFilter: FeedFilter<Note>() {
|
|||||||
&& it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true
|
&& it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true
|
||||||
&& !it.isNewThread()
|
&& !it.isNewThread()
|
||||||
}
|
}
|
||||||
.sortedBy { it.event?.createdAt }
|
.sortedBy { it.createdAt() }
|
||||||
.reversed()
|
.reversed()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -13,7 +13,7 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
|
|||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val user = account.userProfile()
|
val user = account.userProfile()
|
||||||
|
|
||||||
return LocalCache.notes.values
|
val notes = LocalCache.notes.values
|
||||||
.filter {
|
.filter {
|
||||||
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
|
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
|
||||||
&& it.author in user.follows
|
&& it.author in user.follows
|
||||||
@@ -21,7 +21,18 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
|
|||||||
&& it.author?.let { !account.isHidden(it) } ?: true
|
&& it.author?.let { !account.isHidden(it) } ?: true
|
||||||
&& it.isNewThread()
|
&& it.isNewThread()
|
||||||
}
|
}
|
||||||
.sortedBy { it.event?.createdAt }
|
|
||||||
|
val longFormNotes = LocalCache.addressables.values
|
||||||
|
.filter {
|
||||||
|
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
|
||||||
|
&& it.author in user.follows
|
||||||
|
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||||
|
&& it.author?.let { !account.isHidden(it) } ?: true
|
||||||
|
&& it.isNewThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (notes + longFormNotes)
|
||||||
|
.sortedBy { it.createdAt() }
|
||||||
.reversed()
|
.reversed()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -63,7 +63,7 @@ object NotificationFeedFilter: FeedFilter<Note>() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortedBy { it.event?.createdAt }
|
.sortedBy { it.createdAt() }
|
||||||
.reversed()
|
.reversed()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -17,7 +17,7 @@ object UserProfileConversationsFeedFilter: FeedFilter<Note>() {
|
|||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
return user?.notes
|
return user?.notes
|
||||||
?.filter { account?.isAcceptable(it) == true && !it.isNewThread() }
|
?.filter { account?.isAcceptable(it) == true && !it.isNewThread() }
|
||||||
?.sortedBy { it.event?.createdAt }
|
?.sortedBy { it.createdAt() }
|
||||||
?.reversed()
|
?.reversed()
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
}
|
}
|
||||||
|
@@ -15,9 +15,11 @@ object UserProfileNewThreadFeedFilter: FeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
return user?.notes?.plus(user?.longFormNotes?.values?.flatten() ?: emptySet())
|
val longFormNotes = LocalCache.addressables.values.filter { it.author == user }
|
||||||
|
|
||||||
|
return user?.notes?.plus(longFormNotes)
|
||||||
?.filter { account?.isAcceptable(it) == true && it.isNewThread() }
|
?.filter { account?.isAcceptable(it) == true && it.isNewThread() }
|
||||||
?.sortedBy { it.event?.createdAt }
|
?.sortedBy { it.createdAt() }
|
||||||
?.reversed()
|
?.reversed()
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,6 @@ object UserProfileReportsFeedFilter: FeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
return user?.reports?.values?.flatten()?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList()
|
return user?.reports?.values?.flatten()?.sortedBy { it.createdAt() }?.reversed() ?: emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -107,7 +107,7 @@ private fun homeHasNewItems(account: Account, cache: NotificationCache, context:
|
|||||||
|
|
||||||
HomeNewThreadFeedFilter.account = account
|
HomeNewThreadFeedFilter.account = account
|
||||||
|
|
||||||
return (HomeNewThreadFeedFilter.feed().firstOrNull { it.event?.createdAt != null }?.event?.createdAt ?: 0) > lastTime
|
return (HomeNewThreadFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notificationHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean {
|
private fun notificationHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean {
|
||||||
@@ -115,17 +115,17 @@ private fun notificationHasNewItems(account: Account, cache: NotificationCache,
|
|||||||
|
|
||||||
NotificationFeedFilter.account = account
|
NotificationFeedFilter.account = account
|
||||||
|
|
||||||
return (NotificationFeedFilter.feed().firstOrNull { it.event?.createdAt != null }?.event?.createdAt ?: 0) > lastTime
|
return (NotificationFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun messagesHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean {
|
private fun messagesHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean {
|
||||||
ChatroomListKnownFeedFilter.account = account
|
ChatroomListKnownFeedFilter.account = account
|
||||||
|
|
||||||
val note = ChatroomListKnownFeedFilter.feed().firstOrNull {
|
val note = ChatroomListKnownFeedFilter.feed().firstOrNull {
|
||||||
it.event?.createdAt != null && it.channel() == null && it.author != account.userProfile()
|
it.createdAt() != null && it.channel() == null && it.author != account.userProfile()
|
||||||
} ?: return false
|
} ?: return false
|
||||||
|
|
||||||
val lastTime = cache.load("Room/${note.author?.pubkeyHex}", context)
|
val lastTime = cache.load("Room/${note.author?.pubkeyHex}", context)
|
||||||
|
|
||||||
return (note.event?.createdAt ?: 0) > lastTime
|
return (note.createdAt() ?: 0) > lastTime
|
||||||
}
|
}
|
@@ -83,8 +83,8 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
|
|||||||
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = notificationCache) {
|
LaunchedEffect(key1 = notificationCache) {
|
||||||
noteEvent?.let {
|
note.createdAt()?.let {
|
||||||
hasNewMessages = it.createdAt > notificationCache.cache.load("Channel/${channel.idHex}", context)
|
hasNewMessages = it > notificationCache.cache.load("Channel/${channel.idHex}", context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
|
|||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
channelLastTime = note.event?.createdAt,
|
channelLastTime = note.createdAt(),
|
||||||
channelLastContent = "${author?.toBestDisplayName()}: " + description,
|
channelLastContent = "${author?.toBestDisplayName()}: " + description,
|
||||||
hasNewMessages = hasNewMessages,
|
hasNewMessages = hasNewMessages,
|
||||||
onClick = { navController.navigate("Channel/${channel.idHex}") })
|
onClick = { navController.navigate("Channel/${channel.idHex}") })
|
||||||
@@ -134,7 +134,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
|
|||||||
ChannelName(
|
ChannelName(
|
||||||
channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) },
|
channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) },
|
||||||
channelTitle = { UsernameDisplay(userToComposeOn, it) },
|
channelTitle = { UsernameDisplay(userToComposeOn, it) },
|
||||||
channelLastTime = noteEvent?.createdAt,
|
channelLastTime = note.createdAt(),
|
||||||
channelLastContent = accountViewModel.decrypt(note),
|
channelLastContent = accountViewModel.decrypt(note),
|
||||||
hasNewMessages = hasNewMessages,
|
hasNewMessages = hasNewMessages,
|
||||||
onClick = { navController.navigate("Room/${user.pubkeyHex}") })
|
onClick = { navController.navigate("Room/${user.pubkeyHex}") })
|
||||||
|
@@ -134,7 +134,7 @@ fun ChatroomMessageCompose(
|
|||||||
routeForLastRead?.let {
|
routeForLastRead?.let {
|
||||||
val lastTime = NotificationCache.load(it, context)
|
val lastTime = NotificationCache.load(it, context)
|
||||||
|
|
||||||
val createdAt = note.event?.createdAt
|
val createdAt = note.createdAt()
|
||||||
if (createdAt != null) {
|
if (createdAt != null) {
|
||||||
NotificationCache.markAsRead(it, createdAt, context)
|
NotificationCache.markAsRead(it, createdAt, context)
|
||||||
isNew = createdAt > lastTime
|
isNew = createdAt > lastTime
|
||||||
@@ -241,16 +241,16 @@ fun ChatroomMessageCompose(
|
|||||||
val event = note.event
|
val event = note.event
|
||||||
if (event is ChannelCreateEvent) {
|
if (event is ChannelCreateEvent) {
|
||||||
Text(text = note.author?.toBestDisplayName()
|
Text(text = note.author?.toBestDisplayName()
|
||||||
.toString() + " ${stringResource(R.string.created)} " + (event.channelInfo.name
|
.toString() + " ${stringResource(R.string.created)} " + (event.channelInfo().name
|
||||||
?: "") +" ${stringResource(R.string.with_description_of)} '" + (event.channelInfo.about
|
?: "") +" ${stringResource(R.string.with_description_of)} '" + (event.channelInfo().about
|
||||||
?: "") + "', ${stringResource(R.string.and_picture)} '" + (event.channelInfo.picture
|
?: "") + "', ${stringResource(R.string.and_picture)} '" + (event.channelInfo().picture
|
||||||
?: "") + "'"
|
?: "") + "'"
|
||||||
)
|
)
|
||||||
} else if (event is ChannelMetadataEvent) {
|
} else if (event is ChannelMetadataEvent) {
|
||||||
Text(text = note.author?.toBestDisplayName()
|
Text(text = note.author?.toBestDisplayName()
|
||||||
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (event.channelInfo.name
|
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (event.channelInfo().name
|
||||||
?: "") + "$', {stringResource(R.string.description_to)} '" + (event.channelInfo.about
|
?: "") + "$', {stringResource(R.string.description_to)} '" + (event.channelInfo().about
|
||||||
?: "") + "', ${stringResource(R.string.and_picture_to)} '" + (event.channelInfo.picture
|
?: "") + "', ${stringResource(R.string.and_picture_to)} '" + (event.channelInfo().picture
|
||||||
?: "") + "'"
|
?: "") + "'"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -295,7 +295,7 @@ fun ChatroomMessageCompose(
|
|||||||
) {
|
) {
|
||||||
Row() {
|
Row() {
|
||||||
Text(
|
Text(
|
||||||
timeAgoShort(note.event?.createdAt, context),
|
timeAgoShort(note.createdAt(), context),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
)
|
)
|
||||||
|
@@ -107,7 +107,7 @@ fun NoteCompose(
|
|||||||
routeForLastRead?.let {
|
routeForLastRead?.let {
|
||||||
val lastTime = NotificationCache.load(it, context)
|
val lastTime = NotificationCache.load(it, context)
|
||||||
|
|
||||||
val createdAt = noteEvent.createdAt
|
val createdAt = note.createdAt()
|
||||||
if (createdAt != null) {
|
if (createdAt != null) {
|
||||||
NotificationCache.markAsRead(it, createdAt, context)
|
NotificationCache.markAsRead(it, createdAt, context)
|
||||||
isNew = createdAt > lastTime
|
isNew = createdAt > lastTime
|
||||||
@@ -241,7 +241,7 @@ fun NoteCompose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
timeAgo(noteEvent.createdAt, context = context),
|
timeAgo(note.createdAt(), context = context),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
@@ -322,7 +322,7 @@ fun NoteCompose(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (noteEvent is ReportEvent) {
|
} else if (noteEvent is ReportEvent) {
|
||||||
val reportType = (noteEvent.reportedPost + noteEvent.reportedAuthor).map {
|
val reportType = (noteEvent.reportedPost() + noteEvent.reportedAuthor()).map {
|
||||||
when (it.reportType) {
|
when (it.reportType) {
|
||||||
ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content)
|
ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content)
|
||||||
ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity)
|
ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity)
|
||||||
@@ -343,50 +343,7 @@ fun NoteCompose(
|
|||||||
thickness = 0.25.dp
|
thickness = 0.25.dp
|
||||||
)
|
)
|
||||||
} else if (noteEvent is LongTextNoteEvent) {
|
} else if (noteEvent is LongTextNoteEvent) {
|
||||||
Row(
|
LongFormHeader(noteEvent)
|
||||||
modifier = Modifier
|
|
||||||
.clip(shape = RoundedCornerShape(15.dp))
|
|
||||||
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
noteEvent.image?.let {
|
|
||||||
AsyncImage(
|
|
||||||
model = noteEvent.image,
|
|
||||||
contentDescription = stringResource(
|
|
||||||
R.string.preview_card_image_for,
|
|
||||||
noteEvent.image
|
|
||||||
),
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
noteEvent.title?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.body2,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
noteEvent.summary?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.caption,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
|
||||||
color = Color.Gray,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel)
|
||||||
|
|
||||||
@@ -429,6 +386,56 @@ fun NoteCompose(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LongFormHeader(noteEvent: LongTextNoteEvent) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(shape = RoundedCornerShape(15.dp))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||||
|
RoundedCornerShape(15.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
noteEvent.image()?.let {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
R.string.preview_card_image_for,
|
||||||
|
it
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
noteEvent.title()?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
noteEvent.summary()?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RelayBadges(baseNote: Note) {
|
private fun RelayBadges(baseNote: Note) {
|
||||||
val noteRelaysState by baseNote.live().relays.observeAsState()
|
val noteRelaysState by baseNote.live().relays.observeAsState()
|
||||||
|
@@ -10,14 +10,14 @@ abstract class Card() {
|
|||||||
|
|
||||||
class NoteCard(val note: Note): Card() {
|
class NoteCard(val note: Note): Card() {
|
||||||
override fun createdAt(): Long {
|
override fun createdAt(): Long {
|
||||||
return note.event?.createdAt ?: 0
|
return note.createdAt() ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun id() = note.idHex
|
override fun id() = note.idHex
|
||||||
}
|
}
|
||||||
|
|
||||||
class LikeSetCard(val note: Note, val likeEvents: List<Note>): Card() {
|
class LikeSetCard(val note: Note, val likeEvents: List<Note>): Card() {
|
||||||
val createdAt = likeEvents.maxOf { it.event?.createdAt ?: 0 }
|
val createdAt = likeEvents.maxOf { it.createdAt() ?: 0 }
|
||||||
override fun createdAt(): Long {
|
override fun createdAt(): Long {
|
||||||
return createdAt
|
return createdAt
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ class LikeSetCard(val note: Note, val likeEvents: List<Note>): Card() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ZapSetCard(val note: Note, val zapEvents: Map<Note, Note>): Card() {
|
class ZapSetCard(val note: Note, val zapEvents: Map<Note, Note>): Card() {
|
||||||
val createdAt = zapEvents.maxOf { it.value.event?.createdAt ?: 0 }
|
val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 }
|
||||||
override fun createdAt(): Long {
|
override fun createdAt(): Long {
|
||||||
return createdAt
|
return createdAt
|
||||||
}
|
}
|
||||||
@@ -34,9 +34,9 @@ class ZapSetCard(val note: Note, val zapEvents: Map<Note, Note>): Card() {
|
|||||||
|
|
||||||
class MultiSetCard(val note: Note, val boostEvents: List<Note>, val likeEvents: List<Note>, val zapEvents: Map<Note, Note>): Card() {
|
class MultiSetCard(val note: Note, val boostEvents: List<Note>, val likeEvents: List<Note>, val zapEvents: Map<Note, Note>): Card() {
|
||||||
val createdAt = maxOf(
|
val createdAt = maxOf(
|
||||||
zapEvents.maxOfOrNull { it.value.event?.createdAt ?: 0 } ?: 0 ,
|
zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0 ,
|
||||||
likeEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0 ,
|
likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0 ,
|
||||||
boostEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0
|
boostEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun createdAt(): Long {
|
override fun createdAt(): Long {
|
||||||
@@ -46,7 +46,7 @@ class MultiSetCard(val note: Note, val boostEvents: List<Note>, val likeEvents:
|
|||||||
}
|
}
|
||||||
|
|
||||||
class BoostSetCard(val note: Note, val boostEvents: List<Note>): Card() {
|
class BoostSetCard(val note: Note, val boostEvents: List<Note>): Card() {
|
||||||
val createdAt = boostEvents.maxOf { it.event?.createdAt ?: 0 }
|
val createdAt = boostEvents.maxOf { it.createdAt() ?: 0 }
|
||||||
|
|
||||||
override fun createdAt(): Long {
|
override fun createdAt(): Long {
|
||||||
return createdAt
|
return createdAt
|
||||||
|
@@ -97,11 +97,6 @@ private fun FeedLoaded(
|
|||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
delay(500)
|
|
||||||
listState.animateScrollToItem(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(
|
||||||
top = 10.dp,
|
top = 10.dp,
|
||||||
|
@@ -243,7 +243,7 @@ fun NoteMaster(baseNote: Note,
|
|||||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
timeAgo(noteEvent.createdAt, context = context),
|
timeAgo(note.createdAt(), context = context),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
@@ -270,19 +270,19 @@ fun NoteMaster(baseNote: Note,
|
|||||||
if (noteEvent is LongTextNoteEvent) {
|
if (noteEvent is LongTextNoteEvent) {
|
||||||
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) {
|
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) {
|
||||||
Column {
|
Column {
|
||||||
noteEvent.image?.let {
|
noteEvent.image()?.let {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = noteEvent.image,
|
model = it,
|
||||||
contentDescription = stringResource(
|
contentDescription = stringResource(
|
||||||
R.string.preview_card_image_for,
|
R.string.preview_card_image_for,
|
||||||
noteEvent.image
|
it
|
||||||
),
|
),
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.FillWidth,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
noteEvent.title?.let {
|
noteEvent.title()?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontSize = 30.sp,
|
fontSize = 30.sp,
|
||||||
@@ -293,7 +293,7 @@ fun NoteMaster(baseNote: Note,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
noteEvent.summary?.let {
|
noteEvent.summary()?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<string name="scan_qr">Scan QR</string>
|
<string name="scan_qr">Scan QR</string>
|
||||||
<string name="show_anyway">Show Anyway</string>
|
<string name="show_anyway">Show Anyway</string>
|
||||||
<string name="post_was_flagged_as_inappropriate_by">Post was flagged as inappropriate by</string>
|
<string name="post_was_flagged_as_inappropriate_by">Post was flagged as inappropriate by</string>
|
||||||
<string name="post_not_found">post not found</string>
|
<string name="post_not_found">Post not found</string>
|
||||||
<string name="channel_image">Channel Image</string>
|
<string name="channel_image">Channel Image</string>
|
||||||
<string name="referenced_event_not_found">Referenced event not found</string>
|
<string name="referenced_event_not_found">Referenced event not found</string>
|
||||||
<string name="could_not_decrypt_the_message">Could Not decrypt the message</string>
|
<string name="could_not_decrypt_the_message">Could Not decrypt the message</string>
|
||||||
|
@@ -0,0 +1,33 @@
|
|||||||
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.service.Nip19
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ATag
|
||||||
|
import com.vitorpamplona.amethyst.service.toNAddr
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class NIP19ParserTest {
|
||||||
|
@Test
|
||||||
|
fun nAddrParser() {
|
||||||
|
val result = Nip19().uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus")
|
||||||
|
assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nAddrParser2() {
|
||||||
|
val result = Nip19().uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8")
|
||||||
|
assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nAddrFormatter() {
|
||||||
|
val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "" )
|
||||||
|
assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nAddrFormatter2() {
|
||||||
|
val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard" )
|
||||||
|
assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr())
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user