Bringing TextNoteEvent from NostrPostr to Amethyst's codebase and Restructuring Addressable Notes.

This commit is contained in:
Vitor Pamplona
2023-03-02 16:14:06 -05:00
parent bab314ebc9
commit c0bcb638c0
35 changed files with 231 additions and 70 deletions
@@ -34,7 +34,7 @@ import nostr.postr.events.DeletionEvent
import nostr.postr.events.Event import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
import nostr.postr.events.PrivateDmEvent import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import nostr.postr.toHex import nostr.postr.toHex
val DefaultChannels = setOf( val DefaultChannels = setOf(
@@ -290,18 +290,20 @@ 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 signedEvent = TextNoteEvent.create( val signedEvent = TextNoteEvent.create(
msg = message, msg = message,
replyTos = repliesToHex, replyTos = repliesToHex,
mentions = mentionsHex, mentions = mentionsHex,
addresses = addressesHex,
privateKey = loggedIn.privKey!! privateKey = loggedIn.privKey!!
) )
Client.send(signedEvent) Client.send(signedEvent)
LocalCache.consume(signedEvent) LocalCache.consume(signedEvent)
} }
fun sendChannelMeesage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List<User>?) { fun sendChannelMessage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List<User>?) {
if (!isWriteable()) return if (!isWriteable()) return
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -74,6 +74,7 @@ fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
return DirtyKeyInfo("note", keyB32.bechToBytes().toHexKey(), restOfWord) return DirtyKeyInfo("note", keyB32.bechToBytes().toHexKey(), restOfWord)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -37,7 +37,7 @@ import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
import nostr.postr.events.PrivateDmEvent import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.RecommendRelayEvent import nostr.postr.events.RecommendRelayEvent
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import nostr.postr.toHex import nostr.postr.toHex
import nostr.postr.toNpub import nostr.postr.toNpub
@@ -212,7 +212,7 @@ object LocalCache {
//Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content?.take(100)} ${formattedDateTime(event.createdAt)}") //Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content?.take(100)} ${formattedDateTime(event.createdAt)}")
// Prepares user's profile view. // Prepares user's profile view.
author.addNote(note) author.addLongFormNote(note)
// Adds notifications to users. // Adds notifications to users.
mentions.forEach { mentions.forEach {
@@ -222,11 +222,6 @@ object LocalCache {
it.author?.addTaggedPost(note) it.author?.addTaggedPost(note)
} }
// Counts the replies
replyTo.forEach {
it.addReply(note)
}
refreshObservers() refreshObservers()
} }
@@ -492,7 +487,6 @@ object LocalCache {
val note = getOrCreateNote(event.id.toHex()) val note = getOrCreateNote(event.id.toHex())
oldChannel.addNote(note) oldChannel.addNote(note)
note.channel = oldChannel
note.loadEvent(event, author, emptyList(), emptyList()) note.loadEvent(event, author, emptyList(), emptyList())
refreshObservers() refreshObservers()
@@ -514,7 +508,6 @@ object LocalCache {
val note = getOrCreateNote(event.id.toHex()) val note = getOrCreateNote(event.id.toHex())
oldChannel.addNote(note) oldChannel.addNote(note)
note.channel = oldChannel
note.loadEvent(event, author, emptyList(), emptyList()) note.loadEvent(event, author, emptyList(), emptyList())
refreshObservers() refreshObservers()
@@ -553,7 +546,6 @@ object LocalCache {
.mapNotNull { checkGetOrCreateNote(it) } .mapNotNull { checkGetOrCreateNote(it) }
.filter { it.event !is ChannelCreateEvent } .filter { it.event !is ChannelCreateEvent }
note.channel = channel
note.loadEvent(event, author, mentions, replyTo) note.loadEvent(event, author, mentions, replyTo)
//Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content} ${formattedDateTime(event.createdAt)}") //Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content} ${formattedDateTime(event.createdAt)}")
@@ -658,8 +650,9 @@ object LocalCache {
fun findNotesStartingWith(text: String): List<Note> { fun findNotesStartingWith(text: String): List<Note> {
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)
} }
@@ -745,7 +738,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]?.notes ?: emptySet()) + (users[it]?.longFormNotes?.values?.flatten() ?: emptySet())
}.flatten() }.flatten()
account.hiddenUsers.forEach { account.hiddenUsers.forEach {
@@ -754,6 +747,7 @@ 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,7 +2,11 @@ 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.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
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.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
@@ -23,6 +27,7 @@ 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]+)\\]")
@@ -49,14 +54,22 @@ class Note(val idHex: String) {
var relays = setOf<String>() var relays = setOf<String>()
private set private set
var channel: Channel? = null
var lastReactionsDownloadTime: Long? = null var lastReactionsDownloadTime: Long? = null
fun id() = Hex.decode(idHex) fun id() = Hex.decode(idHex)
fun idNote() = id().toNote() fun idNote() = id().toNote()
fun idDisplayNote() = idNote().toShortenHex() fun idDisplayNote() = idNote().toShortenHex()
fun channel(): Channel? {
val channelHex = (event as? ChannelMessageEvent)?.channel ?:
(event as? ChannelMetadataEvent)?.channel ?:
(event as? ChannelCreateEvent)?.let { idHex }
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
}
fun address() = (event as? LongTextNoteEvent)?.address
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
this.author = author this.author = author
@@ -3,6 +3,7 @@ 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
@@ -37,6 +38,8 @@ 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
@@ -142,8 +145,27 @@ 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) {
@@ -11,7 +11,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent import nostr.postr.events.ContactListEvent
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrAccountDataSource: NostrDataSource("AccountData") { object NostrAccountDataSource: NostrDataSource("AccountData") {
lateinit var account: Account lateinit var account: Account
@@ -32,7 +32,7 @@ import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
import nostr.postr.events.PrivateDmEvent import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.RecommendRelayEvent import nostr.postr.events.RecommendRelayEvent
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
abstract class NostrDataSource(val debugName: String) { abstract class NostrDataSource(val debugName: String) {
private var subscriptions = mapOf<String, Subscription>() private var subscriptions = mapOf<String, Subscription>()
@@ -63,12 +63,14 @@ abstract class NostrDataSource(val debugName: String) {
try { try {
when (event) { when (event) {
is MetadataEvent -> LocalCache.consume(event) is MetadataEvent -> LocalCache.consume(event)
is TextNoteEvent -> LocalCache.consume(event, relay) //is TextNoteEvent -> LocalCache.consume(event, relay) overrides default TextNote
is RecommendRelayEvent -> LocalCache.consume(event) is RecommendRelayEvent -> LocalCache.consume(event)
is ContactListEvent -> LocalCache.consume(event) is ContactListEvent -> LocalCache.consume(event)
is PrivateDmEvent -> LocalCache.consume(event, relay) is PrivateDmEvent -> LocalCache.consume(event, relay)
is DeletionEvent -> LocalCache.consume(event) is DeletionEvent -> LocalCache.consume(event)
else -> when (event.kind) { else -> when (event.kind) {
TextNoteEvent.kind -> LocalCache.consume(TextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
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)
@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrGlobalDataSource: NostrDataSource("GlobalFeed") { object NostrGlobalDataSource: NostrDataSource("GlobalFeed") {
fun createGlobalFilter() = TypedFilter( fun createGlobalFilter() = TypedFilter(
@@ -9,7 +9,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrHomeDataSource: NostrDataSource("HomeFeed") { object NostrHomeDataSource: NostrDataSource("HomeFeed") {
lateinit var account: Account lateinit var account: Account
@@ -14,11 +14,37 @@ import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import java.util.Date import java.util.Date
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.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<String>()
private fun createAddressFilter(): List<TypedFilter>? {
val addressesToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }.filter { it.address() != null }
if (addressesToWatch.isEmpty()) {
return null
}
val now = Date().time / 1000
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
)
)
}
}
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? { private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) } val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }
@@ -91,8 +117,9 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
override fun updateChannelFilters() { override fun updateChannelFilters() {
val reactions = createRepliesAndReactionsFilter() val reactions = createRepliesAndReactionsFilter()
val missing = createLoadEventsIfNotLoadedFilter() val missing = createLoadEventsIfNotLoadedFilter()
val addresses = createAddressFilter()
singleEventChannel.typedFilters = listOfNotNull(reactions, missing).flatten().ifEmpty { null } singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses).flatten().ifEmpty { null }
} }
fun add(eventId: String) { fun add(eventId: String) {
@@ -9,7 +9,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent import nostr.postr.events.ContactListEvent
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
var user: User? = null var user: User? = null
@@ -28,11 +28,15 @@ class LnZapRequestEvent (
fun create(originalNote: Event, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { fun create(originalNote: Event, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
val content = "" val content = ""
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey)
val tags = listOf( var tags = listOf(
listOf("e", originalNote.id.toHex()), listOf("e", originalNote.id.toHex()),
listOf("p", originalNote.pubKey.toHex()), listOf("p", originalNote.pubKey.toHex()),
listOf("relays") + relays listOf("relays") + relays
) )
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address) )
}
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event import nostr.postr.events.Event
@@ -20,11 +21,16 @@ class LongTextNoteEvent(
@Transient val summary: String? @Transient val summary: String?
@Transient val publishedAt: Long? @Transient val publishedAt: Long?
@Transient val topics: List<String> @Transient val topics: List<String>
@Transient val address: String
@Transient val dTag: String?
init { init {
replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
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) } topics = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
title = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() title = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
image = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull() image = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
@@ -35,7 +35,12 @@ class ReactionEvent (
fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey)
val 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) {
tags = tags + listOf( listOf("a", originalNote.address) )
}
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ReactionEvent(id, pubKey, createdAt, tags, content, sig) return ReactionEvent(id, pubKey, createdAt, tags, content, sig)
@@ -60,7 +60,12 @@ class ReportEvent (
val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex(), type.name.toLowerCase()) val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex(), type.name.toLowerCase())
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey)
val tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag) var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
if (reportedPost is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", reportedPost.address) )
}
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ReportEvent(id, pubKey, createdAt, tags, content, sig) return ReportEvent(id, pubKey, createdAt, tags, content, sig)
@@ -40,7 +40,12 @@ class RepostEvent (
val replyToAuthor = listOf("p", boostedPost.pubKey.toHex()) val replyToAuthor = listOf("p", boostedPost.pubKey.toHex())
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey)
val 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) {
tags = tags + listOf( listOf("a", boostedPost.address) )
}
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return RepostEvent(id, pubKey, createdAt, tags, content, sig) return RepostEvent(id, pubKey, createdAt, tags, content, sig)
@@ -0,0 +1,45 @@
package com.vitorpamplona.amethyst.service.model
import java.util.Date
import nostr.postr.Utils
import nostr.postr.events.Event
class TextNoteEvent(
id: ByteArray,
pubKey: ByteArray,
createdAt: Long,
tags: List<List<String>>,
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) }
}
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 {
val pubKey = Utils.pubkeyCreate(privateKey)
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it))
}
addresses?.forEach {
tags.add(listOf("a", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
return TextNoteEvent(id, pubKey, createdAt, tags, msg, sig)
}
}
}
@@ -41,7 +41,7 @@ import com.vitorpamplona.amethyst.ui.navigation.UploadFromGallery
import com.vitorpamplona.amethyst.ui.note.ReplyInformation import com.vitorpamplona.amethyst.ui.note.ReplyInformation
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@@ -46,7 +46,6 @@ class NewPostViewModel: ViewModel() {
} else { } else {
this.mentions = currentMentions.plus(replyUser) this.mentions = currentMentions.plus(replyUser)
} }
} }
} }
@@ -68,12 +67,12 @@ class NewPostViewModel: ViewModel() {
fun tagIndex(user: User): Int { fun tagIndex(user: User): Int {
// Postr Events assembles replies before mentions in the tag order // Postr Events assembles replies before mentions in the tag order
return (if (originalNote?.channel != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0) return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0)
} }
fun tagIndex(note: Note): Int { fun tagIndex(note: Note): Int {
// Postr Events assembles replies before mentions in the tag order // Postr Events assembles replies before mentions in the tag order
return (if (originalNote?.channel != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0) return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
} }
fun sendPost() { fun sendPost() {
@@ -108,8 +107,8 @@ class NewPostViewModel: ViewModel() {
}.joinToString(" ") }.joinToString(" ")
}.joinToString("\n") }.joinToString("\n")
if (originalNote?.channel != null) { if (originalNote?.channel() != null) {
account?.sendChannelMeesage(newMessage, originalNote!!.channel!!.idHex, originalNote!!, mentions) account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions)
} else { } else {
account?.sendPost(newMessage, replyTos, mentions) account?.sendPost(newMessage, replyTos, mentions)
} }
@@ -42,10 +42,10 @@ fun ClickableRoute(
onClick = { navController.navigate("Channel/${nip19.hex}") }, onClick = { navController.navigate("Channel/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
) )
} else if (note.channel != null) { } else if (note.channel() != null) {
ClickableText( ClickableText(
text = AnnotatedString("@${note.channel?.toBestDisplayName()} "), text = AnnotatedString("@${note.channel()?.toBestDisplayName()} "),
onClick = { navController.navigate("Channel/${note.channel?.idHex}") }, onClick = { navController.navigate("Channel/${note.channel()?.idHex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
) )
} else { } else {
@@ -26,20 +26,20 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.text.Paragraph import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.RichText
import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.currentRichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults import com.halilibo.richtext.ui.resolveDefaults
import com.halilibo.richtext.ui.string.RichTextString
import com.halilibo.richtext.ui.string.RichTextStringStyle
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.Nip19 import com.vitorpamplona.amethyst.service.Nip19
@@ -81,20 +81,47 @@ fun RichTextViewer(
navController: NavController, navController: NavController,
) { ) {
val myMarkDownStyle = RichTextStyle().resolveDefaults().copy(
codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy(
textStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
),
modifier = Modifier
.padding(0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
)
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f).compositeOver(backgroundColor))
),
stringStyle = RichTextStyle().resolveDefaults().stringStyle?.copy(
linkStyle = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary
),
codeStyle = SpanStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor)
)
)
)
Column(modifier = modifier.animateContentSize()) { Column(modifier = modifier.animateContentSize()) {
if (content.startsWith("# ") || content.contains("##") || content.contains("```")) { if ( content.startsWith("# ")
var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) } || content.contains("##")
|| content.contains("**")
|| content.contains("__")
|| content.contains("```")
) {
MaterialRichText( MaterialRichText(
style = RichTextStyle().resolveDefaults().copy( style = myMarkDownStyle,
stringStyle = richTextStyle.stringStyle?.copy(
linkStyle = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary
)
)
),
) { ) {
Markdown( Markdown(
content = content, content = content,
@@ -4,7 +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 nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object GlobalFeedFilter: FeedFilter<Note>() { object GlobalFeedFilter: FeedFilter<Note>() {
lateinit var account: Account lateinit var account: Account
@@ -4,7 +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.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object HomeConversationsFeedFilter: FeedFilter<Note>() { object HomeConversationsFeedFilter: FeedFilter<Note>() {
lateinit var account: Account lateinit var account: Account
@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object HomeNewThreadFeedFilter: FeedFilter<Note>() { object HomeNewThreadFeedFilter: FeedFilter<Note>() {
lateinit var account: Account lateinit var account: Account
@@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NotificationFeedFilter: FeedFilter<Note>() { object NotificationFeedFilter: FeedFilter<Note>() {
lateinit var account: Account lateinit var account: Account
@@ -15,7 +15,7 @@ object UserProfileNewThreadFeedFilter: FeedFilter<Note>() {
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
return user?.notes return user?.notes?.plus(user?.longFormNotes?.values?.flatten() ?: emptySet())
?.filter { account?.isAcceptable(it) == true && it.isNewThread() } ?.filter { account?.isAcceptable(it) == true && it.isNewThread() }
?.sortedBy { it.event?.createdAt } ?.sortedBy { it.event?.createdAt }
?.reversed() ?.reversed()
@@ -122,7 +122,7 @@ private fun messagesHasNewItems(account: Account, cache: NotificationCache, cont
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.event?.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)
@@ -72,7 +72,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
launchSingleTop = true launchSingleTop = true
} }
} else { } else {
note.channel?.let { note.channel()?.let {
navController.navigate("Channel/${it.idHex}") navController.navigate("Channel/${it.idHex}")
} }
} }
@@ -63,11 +63,11 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
if (note?.event == null) { if (note?.event == null) {
BlankNote(Modifier) BlankNote(Modifier)
} else if (note.channel != null) { } else if (note.channel() != null) {
val authorState by note.author!!.live().metadata.observeAsState() val authorState by note.author!!.live().metadata.observeAsState()
val author = authorState?.user val author = authorState?.user
val channelState by note.channel!!.live.observeAsState() val channelState by note.channel()!!.live.observeAsState()
val channel = channelState?.channel val channel = channelState?.channel
val noteEvent = note.event val noteEvent = note.event
@@ -72,7 +72,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn
launchSingleTop = true launchSingleTop = true
} }
} else { } else {
note.channel?.let { note.channel()?.let {
navController.navigate("Channel/${it.idHex}") navController.navigate("Channel/${it.idHex}")
} }
} }
@@ -78,7 +78,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, modifier: Modifier = Modifier, r
launchSingleTop = true launchSingleTop = true
} }
} else { } else {
note.channel?.let { note.channel()?.let {
navController.navigate("Channel/${it.idHex}") navController.navigate("Channel/${it.idHex}")
} }
} }
@@ -35,6 +35,7 @@ import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
@@ -51,7 +52,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Following import com.vitorpamplona.amethyst.ui.theme.Following
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import nostr.postr.events.PrivateDmEvent import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -133,7 +134,7 @@ fun NoteCompose(
launchSingleTop = true launchSingleTop = true
} }
} else { } else {
note.channel?.let { note.channel()?.let {
navController.navigate("Channel/${it.idHex}") navController.navigate("Channel/${it.idHex}")
} }
} }
@@ -175,7 +176,7 @@ fun NoteCompose(
} }
// boosted picture // boosted picture
val baseChannel = note.channel val baseChannel = note.channel()
if (noteEvent is ChannelMessageEvent && baseChannel != null) { if (noteEvent is ChannelMessageEvent && baseChannel != null) {
val channelState by baseChannel.live.observeAsState() val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel val channel = channelState?.channel
@@ -293,7 +294,7 @@ fun NoteCompose(
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || note.mentions != null)) { } else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || note.mentions != null)) {
val sortedMentions = note.mentions?.toSet()?.sortedBy { account.userProfile().isFollowing(it) } val sortedMentions = note.mentions?.toSet()?.sortedBy { account.userProfile().isFollowing(it) }
note.channel?.let { note.channel()?.let {
ReplyInformationChannel(note.replyTo, sortedMentions, it, navController) ReplyInformationChannel(note.replyTo, sortedMentions, it, navController)
} }
} }
@@ -74,7 +74,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInner
launchSingleTop = true launchSingleTop = true
} }
} else { } else {
note.channel?.let { note.channel()?.let {
navController.navigate("Channel/${it.idHex}") navController.navigate("Channel/${it.idHex}")
} }
} }
@@ -268,7 +268,7 @@ fun NoteMaster(baseNote: Note,
} }
if (noteEvent is LongTextNoteEvent) { if (noteEvent is LongTextNoteEvent) {
Row(modifier = Modifier.padding(horizontal = 12.dp)) { Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) {
Column { Column {
noteEvent.image?.let { noteEvent.image?.let {
AsyncImage( AsyncImage(
@@ -295,7 +295,10 @@ fun NoteMaster(baseNote: Note,
noteEvent.summary?.let { noteEvent.summary?.let {
Text( Text(
text = it text = it,
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp)
) )
} }
} }
@@ -193,7 +193,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
trailingIcon = { trailingIcon = {
PostButton( PostButton(
onPost = { onPost = {
account.sendChannelMeesage(newPost.value.text, channel.idHex, replyTo.value, null) account.sendChannelMessage(newPost.value.text, channel.idHex, replyTo.value, null)
newPost.value = TextFieldValue("") newPost.value = TextFieldValue("")
replyTo.value = null replyTo.value = null
feedViewModel.refresh() // Don't wait a full second before updating feedViewModel.refresh() // Don't wait a full second before updating