kopia lustrzana https://github.com/vitorpamplona/amethyst
Support for Replaceable Events (NIP-33)
rodzic
83f46f1a66
commit
f4d5785710
|
@ -27,6 +27,8 @@ Amethyst brings the best social network to your Android phone. Just insert your
|
|||
- [x] URI Support (NIP-21)
|
||||
- [x] Event Deletion (NIP-09: like, boost, text notes and reports)
|
||||
- [x] Identity Verification (NIP-05)
|
||||
- [x] Long-form Content (NIP-23)
|
||||
- [x] Parameterized Replaceable Events (NIP-33)
|
||||
- [ ] Local Database
|
||||
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
|
||||
- [ ] 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)
|
||||
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
|
||||
- [ ] Events with a Subject (NIP-14)
|
||||
- [ ] Long-form Content (NIP-23)
|
||||
- [ ] Online Relay Search (NIP-50)
|
||||
- [ ] Workspaces
|
||||
- [ ] Expiration Support (NIP-40)
|
||||
|
|
|
@ -290,13 +290,13 @@ class Account(
|
|||
|
||||
val repliesToHex = replyTo?.map { it.idHex }
|
||||
val mentionsHex = mentions?.map { it.pubkeyHex }
|
||||
val addressesHex = replyTo?.mapNotNull { it.address() }
|
||||
val addresses = replyTo?.mapNotNull { it.address() }
|
||||
|
||||
val signedEvent = TextNoteEvent.create(
|
||||
msg = message,
|
||||
replyTos = repliesToHex,
|
||||
mentions = mentionsHex,
|
||||
addresses = addressesHex,
|
||||
addresses = addresses,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
Client.send(signedEvent)
|
||||
|
|
|
@ -51,7 +51,7 @@ class Channel(val idHex: String) {
|
|||
fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
|
||||
val important = notes.values
|
||||
.filter { it.author?.let { it1 -> account.isHidden(it1) } == false }
|
||||
.sortedBy { it.event?.createdAt }
|
||||
.sortedBy { it.createdAt() }
|
||||
.reversed()
|
||||
.take(1000)
|
||||
.toSet()
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
|
|||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
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.ChannelHideMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
|
@ -52,6 +53,7 @@ object LocalCache {
|
|||
val users = ConcurrentHashMap<HexKey, User>()
|
||||
val notes = ConcurrentHashMap<HexKey, Note>()
|
||||
val channels = ConcurrentHashMap<HexKey, Channel>()
|
||||
val addressables = ConcurrentHashMap<String, AddressableNote>()
|
||||
|
||||
fun checkGetOrCreateUser(key: String): User? {
|
||||
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) {
|
||||
// new event
|
||||
|
@ -159,8 +184,9 @@ object LocalCache {
|
|||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
|
||||
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
|
||||
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
|
||||
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||
|
||||
note.loadEvent(event, author, mentions, replyTo)
|
||||
|
||||
|
@ -193,7 +219,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val note = getOrCreateNote(event.id.toHex())
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||
|
||||
if (relay != null) {
|
||||
|
@ -202,27 +228,26 @@ object LocalCache {
|
|||
}
|
||||
|
||||
// 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) }
|
||||
|
||||
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.
|
||||
author.addLongFormNote(note)
|
||||
// Adds notifications to users.
|
||||
mentions.forEach {
|
||||
it.addTaggedPost(note)
|
||||
}
|
||||
replyTo.forEach {
|
||||
it.author?.addTaggedPost(note)
|
||||
}
|
||||
|
||||
// Adds notifications to users.
|
||||
mentions.forEach {
|
||||
it.addTaggedPost(note)
|
||||
refreshObservers()
|
||||
}
|
||||
replyTo.forEach {
|
||||
it.author?.addTaggedPost(note)
|
||||
}
|
||||
|
||||
refreshObservers()
|
||||
}
|
||||
|
||||
private fun findCitations(event: Event): Set<String> {
|
||||
|
@ -245,13 +270,13 @@ object LocalCache {
|
|||
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> {
|
||||
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> {
|
||||
val citations = findCitations(event)
|
||||
|
||||
return event.replyTos.filter { it !in citations }
|
||||
return event.replyTos().filter { it !in citations }
|
||||
}
|
||||
|
||||
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)}")
|
||||
|
||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||
val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = event.boostedPost.mapNotNull { checkGetOrCreateNote(it) }
|
||||
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||
|
||||
note.loadEvent(event, author, mentions, repliesTo)
|
||||
|
||||
|
@ -409,8 +435,9 @@ object LocalCache {
|
|||
if (note.event != null) return
|
||||
|
||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||
val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = event.originalPost.mapNotNull { checkGetOrCreateNote(it) }
|
||||
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||
|
||||
note.loadEvent(event, author, mentions, repliesTo)
|
||||
|
||||
|
@ -459,8 +486,9 @@ object LocalCache {
|
|||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it.key) }
|
||||
val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it.key) }
|
||||
val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) }
|
||||
val repliesTo = event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } +
|
||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||
|
||||
note.loadEvent(event, author, mentions, repliesTo)
|
||||
|
||||
|
@ -483,7 +511,7 @@ object LocalCache {
|
|||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
||||
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())
|
||||
oldChannel.addNote(note)
|
||||
|
@ -496,15 +524,16 @@ object LocalCache {
|
|||
}
|
||||
}
|
||||
fun consume(event: ChannelMetadataEvent) {
|
||||
val channelId = event.channel()
|
||||
//Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
|
||||
if (event.channel.isNullOrBlank()) return
|
||||
if (channelId.isNullOrBlank()) return
|
||||
|
||||
// new event
|
||||
val oldChannel = checkGetOrCreateChannel(event.channel) ?: return
|
||||
val oldChannel = checkGetOrCreateChannel(channelId) ?: return
|
||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
||||
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())
|
||||
oldChannel.addNote(note)
|
||||
|
@ -518,7 +547,9 @@ object LocalCache {
|
|||
}
|
||||
|
||||
fun consume(event: ChannelMessageEvent, relay: Relay?) {
|
||||
if (event.channel.isNullOrBlank()) return
|
||||
val channelId = event.channel()
|
||||
|
||||
if (channelId.isNullOrBlank()) return
|
||||
if (antiSpam.isSpam(event)) {
|
||||
relay?.let {
|
||||
it.spamCounter++
|
||||
|
@ -526,7 +557,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val channel = checkGetOrCreateChannel(event.channel) ?: return
|
||||
val channel = checkGetOrCreateChannel(channelId) ?: return
|
||||
|
||||
val note = getOrCreateNote(event.id.toHex())
|
||||
channel.addNote(note)
|
||||
|
@ -541,8 +572,8 @@ object LocalCache {
|
|||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
|
||||
val replyTo = event.replyTos
|
||||
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
|
||||
val replyTo = event.replyTos()
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
.filter { it.event !is ChannelCreateEvent }
|
||||
|
||||
|
@ -580,13 +611,16 @@ object LocalCache {
|
|||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
val zapRequest = event.containedPost()?.id?.toHexKey()?.let { getOrCreateNote(it) }
|
||||
|
||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||
val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) }
|
||||
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(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)
|
||||
|
||||
val zapRequest = event.containedPost?.id?.toHexKey()?.let { getOrCreateNote(it) }
|
||||
if (zapRequest == null) {
|
||||
Log.e("ZP","Zap Request not found. Unable to process Zap {${event.toJson()}}")
|
||||
return
|
||||
|
@ -617,8 +651,9 @@ object LocalCache {
|
|||
if (note.event != null) return
|
||||
|
||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||
val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) }
|
||||
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||
|
||||
note.loadEvent(event, author, mentions, repliesTo)
|
||||
|
||||
|
@ -652,9 +687,13 @@ object LocalCache {
|
|||
return notes.values.filter {
|
||||
(it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false)
|
||||
|| (it.event is ChannelMessageEvent && it.event?.content?.contains(text, true) ?: false)
|
||||
|| (it.event is LongTextNoteEvent && it.event?.content?.contains(text, true) ?: false)
|
||||
|| it.idHex.startsWith(text, true)
|
||||
|| it.idNote().startsWith(text, true)
|
||||
} + addressables.values.filter {
|
||||
(it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false
|
||||
|| (it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false
|
||||
|| (it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false
|
||||
|| it.idHex.startsWith(text, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -738,7 +777,7 @@ object LocalCache {
|
|||
|
||||
fun pruneHiddenMessages(account: Account) {
|
||||
val toBeRemoved = account.hiddenUsers.map {
|
||||
(users[it]?.notes ?: emptySet()) + (users[it]?.longFormNotes?.values?.flatten() ?: emptySet())
|
||||
(users[it]?.notes ?: emptySet())
|
||||
}.flatten()
|
||||
|
||||
account.hiddenUsers.forEach {
|
||||
|
@ -747,7 +786,6 @@ object LocalCache {
|
|||
|
||||
toBeRemoved.forEach {
|
||||
it.author?.removeNote(it)
|
||||
it.author?.removeLongFormNote(it)
|
||||
|
||||
// reverts the add
|
||||
it.mentions?.forEach { user ->
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.model
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
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.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
|
@ -27,11 +28,18 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nostr.postr.events.Event
|
||||
import nostr.postr.toHex
|
||||
|
||||
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.
|
||||
// They are immutable after that.
|
||||
var event: Event? = null
|
||||
|
@ -57,18 +65,21 @@ class Note(val idHex: String) {
|
|||
var lastReactionsDownloadTime: Long? = null
|
||||
|
||||
fun id() = Hex.decode(idHex)
|
||||
fun idNote() = id().toNote()
|
||||
fun idDisplayNote() = idNote().toShortenHex()
|
||||
open fun idNote() = id().toNote()
|
||||
open fun idDisplayNote() = idNote().toShortenHex()
|
||||
|
||||
fun channel(): Channel? {
|
||||
val channelHex = (event as? ChannelMessageEvent)?.channel ?:
|
||||
(event as? ChannelMetadataEvent)?.channel ?:
|
||||
(event as? ChannelCreateEvent)?.let { idHex }
|
||||
val channelHex =
|
||||
(event as? ChannelMessageEvent)?.channel() ?:
|
||||
(event as? ChannelMetadataEvent)?.channel() ?:
|
||||
(event as? ChannelCreateEvent)?.let { it.id.toHexKey() }
|
||||
|
||||
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>) {
|
||||
this.event = event
|
||||
|
@ -90,14 +101,14 @@ class Note(val idHex: String) {
|
|||
fun replyLevelSignature(cachedSignatures: MutableMap<Note, String> = mutableMapOf()): String {
|
||||
val replyTo = replyTo
|
||||
if (replyTo == null || replyTo.isEmpty()) {
|
||||
return "/" + formattedDateTime(event?.createdAt ?: 0) + ";"
|
||||
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
||||
}
|
||||
|
||||
return replyTo
|
||||
.map {
|
||||
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 {
|
||||
|
@ -236,7 +247,7 @@ class Note(val idHex: String) {
|
|||
val dayAgo = Date().time / 1000 - 24*60*60
|
||||
return reports.isNotEmpty() ||
|
||||
(author?.reports?.values?.filter {
|
||||
it.firstOrNull { ( it.event?.createdAt ?: 0 ) > dayAgo } != null
|
||||
it.firstOrNull { ( it.createdAt() ?: 0 ) > dayAgo } != null
|
||||
}?.isNotEmpty() ?: false)
|
||||
}
|
||||
|
||||
|
@ -283,7 +294,7 @@ class Note(val idHex: String) {
|
|||
|
||||
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
||||
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> {
|
||||
|
@ -356,12 +367,21 @@ class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) {
|
|||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
NostrSingleEventDataSource.add(note.idHex)
|
||||
if (note is AddressableNote) {
|
||||
NostrSingleEventDataSource.addAddress(note)
|
||||
} else {
|
||||
NostrSingleEventDataSource.add(note)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun 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
|
||||
|
||||
import com.vitorpamplona.amethyst.service.model.ATag
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
|
@ -34,7 +35,16 @@ class ThreadAssembler {
|
|||
@OptIn(ExperimentalTime::class)
|
||||
fun findThreadFor(noteId: String): Set<Note> {
|
||||
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) {
|
||||
val thread = mutableSetOf<Note>()
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.model
|
|||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
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.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
|
@ -38,8 +37,7 @@ class User(val pubkeyHex: String) {
|
|||
|
||||
var notes = setOf<Note>()
|
||||
private set
|
||||
var longFormNotes = mapOf<String, Set<Note>>()
|
||||
private set
|
||||
|
||||
var taggedPosts = setOf<Note>()
|
||||
private set
|
||||
|
||||
|
@ -145,27 +143,8 @@ class User(val pubkeyHex: String) {
|
|||
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() {
|
||||
notes = setOf<Note>()
|
||||
longFormNotes = mapOf<String, Set<Note>>()
|
||||
}
|
||||
|
||||
fun addReport(note: Note) {
|
||||
|
@ -179,7 +158,7 @@ class User(val pubkeyHex: String) {
|
|||
liveSet?.reports?.invalidateData()
|
||||
}
|
||||
|
||||
val reportTime = note.event?.createdAt ?: 0
|
||||
val reportTime = note.createdAt() ?: 0
|
||||
if (reportTime > latestReportTime) {
|
||||
latestReportTime = reportTime
|
||||
}
|
||||
|
@ -311,7 +290,7 @@ class User(val pubkeyHex: String) {
|
|||
|
||||
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -364,6 +343,7 @@ data class RelayInfo (
|
|||
|
||||
data class Chatroom(var roomMessages: Set<Note>)
|
||||
|
||||
|
||||
class UserMetadata {
|
||||
var name: String? = null
|
||||
var username: String? = null
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.toByteArray
|
||||
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.toByteArray
|
||||
|
||||
class Nip19 {
|
||||
|
||||
enum class Type {
|
||||
USER, NOTE
|
||||
USER, NOTE, RELAY, ADDRESS
|
||||
}
|
||||
data class Return(val type: Type, val hex: String)
|
||||
|
||||
|
@ -24,16 +30,31 @@ class Nip19 {
|
|||
}
|
||||
if (key.startsWith("nprofile")) {
|
||||
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)
|
||||
return Return(Type.USER, hex)
|
||||
}
|
||||
if (key.startsWith("nevent")) {
|
||||
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)
|
||||
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) {
|
||||
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
|
||||
|
@ -42,22 +63,34 @@ class Nip19 {
|
|||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin
|
||||
SPECIAL(0),
|
||||
RELAY(1),
|
||||
AUTHOR(2),
|
||||
KIND(3);
|
||||
}
|
||||
|
||||
if (!result.containsKey(t)) {
|
||||
result.put(t, mutableListOf())
|
||||
}
|
||||
result.get(t)?.add(v)
|
||||
fun toInt32(bytes: ByteArray): Int {
|
||||
require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" }
|
||||
return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int
|
||||
}
|
||||
|
||||
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 -> {
|
||||
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)
|
||||
}
|
||||
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 -> {
|
||||
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)
|
||||
}
|
||||
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
|
||||
|
||||
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.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
|
@ -17,10 +17,11 @@ import nostr.postr.JsonFilter
|
|||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
|
||||
object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
||||
private var eventsToWatch = setOf<String>()
|
||||
private var eventsToWatch = setOf<Note>()
|
||||
private var addressesToWatch = setOf<Note>()
|
||||
|
||||
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()) {
|
||||
return null
|
||||
|
@ -31,22 +32,24 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||
return addressesToWatch.filter {
|
||||
val lastTime = it.lastReactionsDownloadTime
|
||||
lastTime == null || lastTime < (now - 10)
|
||||
}.map {
|
||||
TypedFilter(
|
||||
types = FeedType.values().toSet(),
|
||||
filter = JsonFilter(
|
||||
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
|
||||
}.mapNotNull {
|
||||
it.address()?.let { aTag ->
|
||||
TypedFilter(
|
||||
types = FeedType.values().toSet(),
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(
|
||||
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind
|
||||
),
|
||||
tags = mapOf("a" to listOf(aTag.toTag())),
|
||||
since = it.lastReactionsDownloadTime
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
|
||||
val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }
|
||||
val reactionsToWatch = eventsToWatch
|
||||
|
||||
if (reactionsToWatch.isEmpty()) {
|
||||
return null
|
||||
|
@ -73,11 +76,9 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||
|
||||
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
|
||||
val directEventsToLoad = eventsToWatch
|
||||
.map { LocalCache.getOrCreateNote(it) }
|
||||
.filter { it.event == null }
|
||||
|
||||
val threadingEventsToLoad = eventsToWatch
|
||||
.map { LocalCache.getOrCreateNote(it) }
|
||||
.mapNotNull { it.replyTo }
|
||||
.flatten()
|
||||
.filter { it.event == null }
|
||||
|
@ -107,7 +108,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||
|
||||
val singleEventChannel = requestNewChannel { time ->
|
||||
eventsToWatch.forEach {
|
||||
LocalCache.getOrCreateNote(it).lastReactionsDownloadTime = time
|
||||
it.lastReactionsDownloadTime = time
|
||||
}
|
||||
// Many relays operate with limits in the amount of filters.
|
||||
// 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 }
|
||||
}
|
||||
|
||||
fun add(eventId: String) {
|
||||
fun add(eventId: Note) {
|
||||
eventsToWatch = eventsToWatch.plus(eventId)
|
||||
invalidateFilters()
|
||||
}
|
||||
|
||||
fun remove(eventId: String) {
|
||||
fun remove(eventId: Note) {
|
||||
eventsToWatch = eventsToWatch.minus(eventId)
|
||||
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,
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient val channelInfo: ChannelData
|
||||
|
||||
init {
|
||||
channelInfo = try {
|
||||
MetadataEvent.gson.fromJson(content, ChannelData::class.java)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
|
||||
ChannelData(null, null, null)
|
||||
}
|
||||
fun channelInfo() = try {
|
||||
MetadataEvent.gson.fromJson(content, ChannelData::class.java)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
|
||||
ChannelData(null, null, null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -12,11 +12,7 @@ class ChannelHideMessageEvent (
|
|||
content: String,
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient val eventsToHide: List<String>
|
||||
|
||||
init {
|
||||
eventsToHide = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
}
|
||||
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
companion object {
|
||||
const val kind = 43
|
||||
|
|
|
@ -12,15 +12,10 @@ class ChannelMessageEvent (
|
|||
content: String,
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient val channel: String?
|
||||
@Transient val replyTos: List<String>
|
||||
@Transient val mentions: List<String>
|
||||
|
||||
init {
|
||||
channel = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
||||
replyTos = tags.filter { it.getOrNull(1) != channel }.mapNotNull { it.getOrNull(1) }
|
||||
mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
}
|
||||
fun 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) }
|
||||
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
companion object {
|
||||
const val kind = 42
|
||||
|
|
|
@ -14,19 +14,14 @@ class ChannelMetadataEvent (
|
|||
content: String,
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient val channel: String?
|
||||
@Transient val channelInfo: ChannelCreateEvent.ChannelData
|
||||
|
||||
init {
|
||||
channel = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
||||
channelInfo =
|
||||
try {
|
||||
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)
|
||||
}
|
||||
}
|
||||
fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
||||
fun channelInfo() =
|
||||
try {
|
||||
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 {
|
||||
const val kind = 41
|
||||
|
|
|
@ -12,11 +12,9 @@ class ChannelMuteUserEvent (
|
|||
content: String,
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient val usersToMute: List<String>
|
||||
|
||||
init {
|
||||
usersToMute = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
}
|
||||
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
|
||||
companion object {
|
||||
const val kind = 44
|
||||
|
|
|
@ -14,31 +14,25 @@ class LnZapEvent (
|
|||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
@Transient val zappedPost: List<String>
|
||||
@Transient val zappedAuthor: List<String>
|
||||
@Transient val containedPost: Event?
|
||||
@Transient val lnInvoice: String?
|
||||
@Transient val preimage: String?
|
||||
@Transient val amount: BigDecimal?
|
||||
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
init {
|
||||
zappedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
zappedAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||
|
||||
lnInvoice = tags.filter { it.firstOrNull() == "bolt11" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||
amount = lnInvoice?.let { LnInvoiceUtil.getAmountInSats(lnInvoice) }
|
||||
preimage = tags.filter { it.firstOrNull() == "preimage" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||
fun lnInvoice() = tags.filter { it.firstOrNull() == "bolt11" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||
fun 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 {
|
||||
if (description == null)
|
||||
null
|
||||
else
|
||||
fromJson(description, Client.lenient)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
// Keeps this as a field because it's a heavier function used everywhere.
|
||||
val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
|
||||
|
||||
fun containedPost() = try {
|
||||
description()?.let {
|
||||
fromJson(it, Client.lenient)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -13,14 +13,9 @@ class LnZapRequestEvent (
|
|||
content: String,
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
@Transient val zappedPost: List<String>
|
||||
@Transient val zappedAuthor: List<String>
|
||||
|
||||
init {
|
||||
zappedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
zappedAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
}
|
||||
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||
|
||||
companion object {
|
||||
const val kind = 9734
|
||||
|
@ -34,7 +29,7 @@ class LnZapRequestEvent (
|
|||
listOf("relays") + relays
|
||||
)
|
||||
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)
|
||||
|
|
|
@ -13,33 +13,21 @@ class LongTextNoteEvent(
|
|||
content: String,
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient val replyTos: List<String>
|
||||
@Transient val mentions: List<String>
|
||||
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
@Transient val title: String?
|
||||
@Transient val image: String?
|
||||
@Transient val summary: String?
|
||||
@Transient val publishedAt: Long?
|
||||
@Transient val topics: List<String>
|
||||
@Transient val address: String
|
||||
@Transient val dTag: String?
|
||||
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
||||
fun address() = ATag(kind, pubKey.toHexKey(), dTag())
|
||||
|
||||
init {
|
||||
replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
||||
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||
fun 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()
|
||||
|
||||
address = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "$kind:${pubKey.toHexKey()}:$dTag"
|
||||
topics = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
||||
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
|
||||
}
|
||||
fun publishedAt() = try {
|
||||
tags.filter { it.firstOrNull() == "published_at" }.mapNotNull { it.getOrNull(1) }.firstOrNull()?.toLong()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -59,4 +47,4 @@ class LongTextNoteEvent(
|
|||
return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,13 +14,9 @@ class ReactionEvent (
|
|||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
@Transient val originalPost: List<String>
|
||||
@Transient val originalAuthor: List<String>
|
||||
|
||||
init {
|
||||
originalPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
originalAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
}
|
||||
fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||
|
||||
companion object {
|
||||
const val kind = 7
|
||||
|
@ -38,7 +34,7 @@ class ReactionEvent (
|
|||
|
||||
var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex()))
|
||||
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)
|
||||
|
|
|
@ -17,12 +17,8 @@ class ReportEvent (
|
|||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
@Transient val reportedPost: List<ReportedKey>
|
||||
@Transient val reportedAuthor: List<ReportedKey>
|
||||
|
||||
init {
|
||||
private fun defaultReportType(): ReportType {
|
||||
// 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()
|
||||
if (reportType == null) {
|
||||
reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull()
|
||||
|
@ -30,26 +26,29 @@ class ReportEvent (
|
|||
if (reportType == null) {
|
||||
reportType = ReportType.SPAM
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
return reportType
|
||||
}
|
||||
|
||||
fun reportedPost() = tags
|
||||
.filter { it.firstOrNull() == "e" && it.getOrNull(1) != null }
|
||||
.map {
|
||||
ReportedKey(
|
||||
it[1],
|
||||
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType()
|
||||
)
|
||||
}
|
||||
|
||||
fun reportedAuthor() = tags
|
||||
.filter { it.firstOrNull() == "p" && it.getOrNull(1) != null }
|
||||
.map {
|
||||
ReportedKey(
|
||||
it[1],
|
||||
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType()
|
||||
)
|
||||
}
|
||||
|
||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||
|
||||
companion object {
|
||||
const val kind = 1984
|
||||
|
||||
|
@ -63,7 +62,7 @@ class ReportEvent (
|
|||
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
|
||||
|
||||
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)
|
||||
|
|
|
@ -15,19 +15,15 @@ class RepostEvent (
|
|||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
@Transient val boostedPost: List<String>
|
||||
@Transient val originalAuthor: List<String>
|
||||
@Transient val containedPost: Event?
|
||||
|
||||
init {
|
||||
boostedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
originalAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||
|
||||
containedPost = try {
|
||||
fromJson(content, Client.lenient)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
fun containedPost() = try {
|
||||
fromJson(content, Client.lenient)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -43,7 +39,7 @@ class RepostEvent (
|
|||
var tags:List<List<String>> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor))
|
||||
|
||||
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)
|
||||
|
|
|
@ -12,20 +12,14 @@ class TextNoteEvent(
|
|||
content: String,
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient val replyTos: List<String>
|
||||
@Transient val mentions: List<String>
|
||||
@Transient val longFormAddress: List<String>
|
||||
|
||||
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) }
|
||||
}
|
||||
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
companion object {
|
||||
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 tags = mutableListOf<List<String>>()
|
||||
replyTos?.forEach {
|
||||
|
@ -35,7 +29,7 @@ class TextNoteEvent(
|
|||
tags.add(listOf("p", it))
|
||||
}
|
||||
addresses?.forEach {
|
||||
tags.add(listOf("a", it))
|
||||
tags.add(listOf("a", it.toTag()))
|
||||
}
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
|
|
|
@ -16,6 +16,6 @@ object ChannelFeedFilter: FeedFilter<Note>() {
|
|||
|
||||
// returns the last Note of each user.
|
||||
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()
|
||||
|
||||
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 {
|
||||
privateChatrooms[it]?.roomMessages?.sortedBy {
|
||||
it.event?.createdAt
|
||||
it.createdAt()
|
||||
}?.lastOrNull {
|
||||
it.event != null
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
privateChatrooms[it]?.roomMessages?.sortedBy {
|
||||
it.event?.createdAt
|
||||
it.createdAt()
|
||||
}?.lastOrNull {
|
||||
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.Note
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
|
||||
object GlobalFeedFilter: FeedFilter<Note>() {
|
||||
|
@ -11,11 +12,17 @@ object GlobalFeedFilter: FeedFilter<Note>() {
|
|||
|
||||
override fun feed() = LocalCache.notes.values
|
||||
.filter {
|
||||
(it.event is TextNoteEvent && (it.event as TextNoteEvent).replyTos.isEmpty()) ||
|
||||
(it.event is ChannelMessageEvent && (it.event as ChannelMessageEvent).replyTos.isEmpty())
|
||||
(it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent)
|
||||
&& 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) }
|
||||
.sortedBy { it.event?.createdAt }
|
||||
.sortedBy { it.createdAt() }
|
||||
.reversed()
|
||||
|
||||
}
|
|
@ -20,7 +20,7 @@ object HomeConversationsFeedFilter: FeedFilter<Note>() {
|
|||
&& it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true
|
||||
&& !it.isNewThread()
|
||||
}
|
||||
.sortedBy { it.event?.createdAt }
|
||||
.sortedBy { it.createdAt() }
|
||||
.reversed()
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
|
|||
override fun feed(): List<Note> {
|
||||
val user = account.userProfile()
|
||||
|
||||
return LocalCache.notes.values
|
||||
val notes = LocalCache.notes.values
|
||||
.filter {
|
||||
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
|
||||
&& it.author in user.follows
|
||||
|
@ -21,7 +21,18 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
|
|||
&& it.author?.let { !account.isHidden(it) } ?: true
|
||||
&& 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()
|
||||
}
|
||||
}
|
|
@ -63,7 +63,7 @@ object NotificationFeedFilter: FeedFilter<Note>() {
|
|||
)
|
||||
}
|
||||
|
||||
.sortedBy { it.event?.createdAt }
|
||||
.sortedBy { it.createdAt() }
|
||||
.reversed()
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ object UserProfileConversationsFeedFilter: FeedFilter<Note>() {
|
|||
override fun feed(): List<Note> {
|
||||
return user?.notes
|
||||
?.filter { account?.isAcceptable(it) == true && !it.isNewThread() }
|
||||
?.sortedBy { it.event?.createdAt }
|
||||
?.sortedBy { it.createdAt() }
|
||||
?.reversed()
|
||||
?: emptyList()
|
||||
}
|
||||
|
|
|
@ -15,9 +15,11 @@ object UserProfileNewThreadFeedFilter: FeedFilter<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() }
|
||||
?.sortedBy { it.event?.createdAt }
|
||||
?.sortedBy { it.createdAt() }
|
||||
?.reversed()
|
||||
?: emptyList()
|
||||
}
|
||||
|
|
|
@ -12,6 +12,6 @@ object UserProfileReportsFeedFilter: FeedFilter<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
|
||||
|
||||
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 {
|
||||
|
@ -115,17 +115,17 @@ private fun notificationHasNewItems(account: Account, cache: NotificationCache,
|
|||
|
||||
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 {
|
||||
ChatroomListKnownFeedFilter.account = account
|
||||
|
||||
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
|
||||
|
||||
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) }
|
||||
|
||||
LaunchedEffect(key1 = notificationCache) {
|
||||
noteEvent?.let {
|
||||
hasNewMessages = it.createdAt > notificationCache.cache.load("Channel/${channel.idHex}", context)
|
||||
note.createdAt()?.let {
|
||||
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)
|
||||
)
|
||||
},
|
||||
channelLastTime = note.event?.createdAt,
|
||||
channelLastTime = note.createdAt(),
|
||||
channelLastContent = "${author?.toBestDisplayName()}: " + description,
|
||||
hasNewMessages = hasNewMessages,
|
||||
onClick = { navController.navigate("Channel/${channel.idHex}") })
|
||||
|
@ -134,7 +134,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
|
|||
ChannelName(
|
||||
channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) },
|
||||
channelTitle = { UsernameDisplay(userToComposeOn, it) },
|
||||
channelLastTime = noteEvent?.createdAt,
|
||||
channelLastTime = note.createdAt(),
|
||||
channelLastContent = accountViewModel.decrypt(note),
|
||||
hasNewMessages = hasNewMessages,
|
||||
onClick = { navController.navigate("Room/${user.pubkeyHex}") })
|
||||
|
|
|
@ -134,7 +134,7 @@ fun ChatroomMessageCompose(
|
|||
routeForLastRead?.let {
|
||||
val lastTime = NotificationCache.load(it, context)
|
||||
|
||||
val createdAt = note.event?.createdAt
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
NotificationCache.markAsRead(it, createdAt, context)
|
||||
isNew = createdAt > lastTime
|
||||
|
@ -241,16 +241,16 @@ fun ChatroomMessageCompose(
|
|||
val event = note.event
|
||||
if (event is ChannelCreateEvent) {
|
||||
Text(text = note.author?.toBestDisplayName()
|
||||
.toString() + " ${stringResource(R.string.created)} " + (event.channelInfo.name
|
||||
?: "") +" ${stringResource(R.string.with_description_of)} '" + (event.channelInfo.about
|
||||
?: "") + "', ${stringResource(R.string.and_picture)} '" + (event.channelInfo.picture
|
||||
.toString() + " ${stringResource(R.string.created)} " + (event.channelInfo().name
|
||||
?: "") +" ${stringResource(R.string.with_description_of)} '" + (event.channelInfo().about
|
||||
?: "") + "', ${stringResource(R.string.and_picture)} '" + (event.channelInfo().picture
|
||||
?: "") + "'"
|
||||
)
|
||||
} else if (event is ChannelMetadataEvent) {
|
||||
Text(text = note.author?.toBestDisplayName()
|
||||
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (event.channelInfo.name
|
||||
?: "") + "$', {stringResource(R.string.description_to)} '" + (event.channelInfo.about
|
||||
?: "") + "', ${stringResource(R.string.and_picture_to)} '" + (event.channelInfo.picture
|
||||
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (event.channelInfo().name
|
||||
?: "") + "$', {stringResource(R.string.description_to)} '" + (event.channelInfo().about
|
||||
?: "") + "', ${stringResource(R.string.and_picture_to)} '" + (event.channelInfo().picture
|
||||
?: "") + "'"
|
||||
)
|
||||
} else {
|
||||
|
@ -295,7 +295,7 @@ fun ChatroomMessageCompose(
|
|||
) {
|
||||
Row() {
|
||||
Text(
|
||||
timeAgoShort(note.event?.createdAt, context),
|
||||
timeAgoShort(note.createdAt(), context),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
|
|
|
@ -107,7 +107,7 @@ fun NoteCompose(
|
|||
routeForLastRead?.let {
|
||||
val lastTime = NotificationCache.load(it, context)
|
||||
|
||||
val createdAt = noteEvent.createdAt
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
NotificationCache.markAsRead(it, createdAt, context)
|
||||
isNew = createdAt > lastTime
|
||||
|
@ -241,7 +241,7 @@ fun NoteCompose(
|
|||
}
|
||||
|
||||
Text(
|
||||
timeAgo(noteEvent.createdAt, context = context),
|
||||
timeAgo(note.createdAt(), context = context),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
maxLines = 1
|
||||
)
|
||||
|
@ -322,7 +322,7 @@ fun NoteCompose(
|
|||
)
|
||||
}
|
||||
} else if (noteEvent is ReportEvent) {
|
||||
val reportType = (noteEvent.reportedPost + noteEvent.reportedAuthor).map {
|
||||
val reportType = (noteEvent.reportedPost() + noteEvent.reportedAuthor()).map {
|
||||
when (it.reportType) {
|
||||
ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content)
|
||||
ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity)
|
||||
|
@ -343,50 +343,7 @@ fun NoteCompose(
|
|||
thickness = 0.25.dp
|
||||
)
|
||||
} else if (noteEvent is 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 = 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LongFormHeader(noteEvent)
|
||||
|
||||
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
|
||||
private fun RelayBadges(baseNote: Note) {
|
||||
val noteRelaysState by baseNote.live().relays.observeAsState()
|
||||
|
|
|
@ -10,14 +10,14 @@ abstract class Card() {
|
|||
|
||||
class NoteCard(val note: Note): Card() {
|
||||
override fun createdAt(): Long {
|
||||
return note.event?.createdAt ?: 0
|
||||
return note.createdAt() ?: 0
|
||||
}
|
||||
|
||||
override fun id() = note.idHex
|
||||
}
|
||||
|
||||
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 {
|
||||
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() {
|
||||
val createdAt = zapEvents.maxOf { it.value.event?.createdAt ?: 0 }
|
||||
val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 }
|
||||
override fun createdAt(): Long {
|
||||
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() {
|
||||
val createdAt = maxOf(
|
||||
zapEvents.maxOfOrNull { it.value.event?.createdAt ?: 0 } ?: 0 ,
|
||||
likeEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0 ,
|
||||
boostEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0
|
||||
zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0 ,
|
||||
likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0 ,
|
||||
boostEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0
|
||||
)
|
||||
|
||||
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() {
|
||||
val createdAt = boostEvents.maxOf { it.event?.createdAt ?: 0 }
|
||||
val createdAt = boostEvents.maxOf { it.createdAt() ?: 0 }
|
||||
|
||||
override fun createdAt(): Long {
|
||||
return createdAt
|
||||
|
|
|
@ -97,11 +97,6 @@ private fun FeedLoaded(
|
|||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(500)
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp,
|
||||
|
|
|
@ -243,7 +243,7 @@ fun NoteMaster(baseNote: Note,
|
|||
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
||||
|
||||
Text(
|
||||
timeAgo(noteEvent.createdAt, context = context),
|
||||
timeAgo(note.createdAt(), context = context),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
maxLines = 1
|
||||
)
|
||||
|
@ -270,19 +270,19 @@ fun NoteMaster(baseNote: Note,
|
|||
if (noteEvent is LongTextNoteEvent) {
|
||||
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) {
|
||||
Column {
|
||||
noteEvent.image?.let {
|
||||
noteEvent.image()?.let {
|
||||
AsyncImage(
|
||||
model = noteEvent.image,
|
||||
model = it,
|
||||
contentDescription = stringResource(
|
||||
R.string.preview_card_image_for,
|
||||
noteEvent.image
|
||||
it
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
noteEvent.title?.let {
|
||||
noteEvent.title()?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 30.sp,
|
||||
|
@ -293,7 +293,7 @@ fun NoteMaster(baseNote: Note,
|
|||
)
|
||||
}
|
||||
|
||||
noteEvent.summary?.let {
|
||||
noteEvent.summary()?.let {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<string name="scan_qr">Scan QR</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_not_found">post not found</string>
|
||||
<string name="post_not_found">Post not found</string>
|
||||
<string name="channel_image">Channel Image</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>
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue