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.MetadataEvent
import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import nostr.postr.toHex
val DefaultChannels = setOf(
@@ -290,18 +290,20 @@ class Account(
val repliesToHex = replyTo?.map { it.idHex }
val mentionsHex = mentions?.map { it.pubkeyHex }
val addressesHex = replyTo?.mapNotNull { it.address() }
val signedEvent = TextNoteEvent.create(
msg = message,
replyTos = repliesToHex,
mentions = mentionsHex,
addresses = addressesHex,
privateKey = loggedIn.privKey!!
)
Client.send(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
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -74,6 +74,7 @@ fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
return DirtyKeyInfo("note", keyB32.bechToBytes().toHexKey(), restOfWord)
}
} catch (e: Exception) {
e.printStackTrace()
}
@@ -37,7 +37,7 @@ import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent
import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.RecommendRelayEvent
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import nostr.postr.toHex
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)}")
// Prepares user's profile view.
author.addNote(note)
author.addLongFormNote(note)
// Adds notifications to users.
mentions.forEach {
@@ -222,11 +222,6 @@ object LocalCache {
it.author?.addTaggedPost(note)
}
// Counts the replies
replyTo.forEach {
it.addReply(note)
}
refreshObservers()
}
@@ -492,7 +487,6 @@ object LocalCache {
val note = getOrCreateNote(event.id.toHex())
oldChannel.addNote(note)
note.channel = oldChannel
note.loadEvent(event, author, emptyList(), emptyList())
refreshObservers()
@@ -514,7 +508,6 @@ object LocalCache {
val note = getOrCreateNote(event.id.toHex())
oldChannel.addNote(note)
note.channel = oldChannel
note.loadEvent(event, author, emptyList(), emptyList())
refreshObservers()
@@ -553,7 +546,6 @@ object LocalCache {
.mapNotNull { checkGetOrCreateNote(it) }
.filter { it.event !is ChannelCreateEvent }
note.channel = channel
note.loadEvent(event, author, mentions, replyTo)
//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> {
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 LongTextNoteEvent && it.event?.content?.contains(text, true) ?: false)
|| it.idHex.startsWith(text, true)
|| it.idNote().startsWith(text, true)
}
@@ -745,7 +738,7 @@ object LocalCache {
fun pruneHiddenMessages(account: Account) {
val toBeRemoved = account.hiddenUsers.map {
users[it]?.notes ?: emptySet()
(users[it]?.notes ?: emptySet()) + (users[it]?.longFormNotes?.values?.flatten() ?: emptySet())
}.flatten()
account.hiddenUsers.forEach {
@@ -754,6 +747,7 @@ object LocalCache {
toBeRemoved.forEach {
it.author?.removeNote(it)
it.author?.removeLongFormNote(it)
// reverts the add
it.mentions?.forEach { user ->
@@ -2,7 +2,11 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData
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.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Relay
@@ -23,6 +27,7 @@ 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]+)\\]")
@@ -49,14 +54,22 @@ class Note(val idHex: String) {
var relays = setOf<String>()
private set
var channel: Channel? = null
var lastReactionsDownloadTime: Long? = null
fun id() = Hex.decode(idHex)
fun idNote() = id().toNote()
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>) {
this.event = event
this.author = author
@@ -3,6 +3,7 @@ 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
@@ -37,6 +38,8 @@ 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
@@ -142,8 +145,27 @@ 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) {
@@ -11,7 +11,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent
import nostr.postr.events.MetadataEvent
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrAccountDataSource: NostrDataSource("AccountData") {
lateinit var account: Account
@@ -32,7 +32,7 @@ import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent
import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.RecommendRelayEvent
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
abstract class NostrDataSource(val debugName: String) {
private var subscriptions = mapOf<String, Subscription>()
@@ -63,12 +63,14 @@ abstract class NostrDataSource(val debugName: String) {
try {
when (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 ContactListEvent -> LocalCache.consume(event)
is PrivateDmEvent -> LocalCache.consume(event, relay)
is DeletionEvent -> LocalCache.consume(event)
else -> when (event.kind) {
TextNoteEvent.kind -> LocalCache.consume(TextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
RepostEvent.kind -> {
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.TypedFilter
import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrGlobalDataSource: NostrDataSource("GlobalFeed") {
fun createGlobalFilter() = TypedFilter(
@@ -9,7 +9,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrHomeDataSource: NostrDataSource("HomeFeed") {
lateinit var account: Account
@@ -14,11 +14,37 @@ import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import java.util.Date
import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
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>? {
val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }
@@ -91,8 +117,9 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
override fun updateChannelFilters() {
val reactions = createRepliesAndReactionsFilter()
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) {
@@ -9,7 +9,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent
import nostr.postr.events.MetadataEvent
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
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 {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey)
val tags = listOf(
var tags = listOf(
listOf("e", originalNote.id.toHex()),
listOf("p", originalNote.pubKey.toHex()),
listOf("relays") + relays
)
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date
import nostr.postr.Utils
import nostr.postr.events.Event
@@ -20,11 +21,16 @@ class LongTextNoteEvent(
@Transient val summary: String?
@Transient val publishedAt: Long?
@Transient val topics: List<String>
@Transient val address: String
@Transient val dTag: String?
init {
replyTos = tags.filter { it.firstOrNull() == "e" }.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) }
title = tags.filter { it.firstOrNull() == "title" }.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 {
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 sig = Utils.sign(id, privateKey)
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 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 sig = Utils.sign(id, privateKey)
return ReportEvent(id, pubKey, createdAt, tags, content, sig)
@@ -40,7 +40,12 @@ class RepostEvent (
val replyToAuthor = listOf("p", boostedPost.pubKey.toHex())
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 sig = Utils.sign(id, privateKey)
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.screen.loggedIn.UserLine
import kotlinx.coroutines.delay
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
@OptIn(ExperimentalComposeUiApi::class)
@@ -46,7 +46,6 @@ class NewPostViewModel: ViewModel() {
} else {
this.mentions = currentMentions.plus(replyUser)
}
}
}
@@ -68,12 +67,12 @@ class NewPostViewModel: ViewModel() {
fun tagIndex(user: User): Int {
// 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 {
// 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() {
@@ -108,8 +107,8 @@ class NewPostViewModel: ViewModel() {
}.joinToString(" ")
}.joinToString("\n")
if (originalNote?.channel != null) {
account?.sendChannelMeesage(newMessage, originalNote!!.channel!!.idHex, originalNote!!, mentions)
if (originalNote?.channel() != null) {
account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions)
} else {
account?.sendPost(newMessage, replyTos, mentions)
}
@@ -42,10 +42,10 @@ fun ClickableRoute(
onClick = { navController.navigate("Channel/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else if (note.channel != null) {
} else if (note.channel() != null) {
ClickableText(
text = AnnotatedString("@${note.channel?.toBestDisplayName()} "),
onClick = { navController.navigate("Channel/${note.channel?.idHex}") },
text = AnnotatedString("@${note.channel()?.toBestDisplayName()} "),
onClick = { navController.navigate("Channel/${note.channel()?.idHex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else {
@@ -26,20 +26,20 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.text.Paragraph
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.TextDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.google.accompanist.flowlayout.FlowRow
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.RichText
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.currentRichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText
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.model.LocalCache
import com.vitorpamplona.amethyst.service.Nip19
@@ -81,20 +81,47 @@ fun RichTextViewer(
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()) {
if (content.startsWith("# ") || content.contains("##") || content.contains("```")) {
var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) }
if ( content.startsWith("# ")
|| content.contains("##")
|| content.contains("**")
|| content.contains("__")
|| content.contains("```")
) {
MaterialRichText(
style = RichTextStyle().resolveDefaults().copy(
stringStyle = richTextStyle.stringStyle?.copy(
linkStyle = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary
)
)
),
style = myMarkDownStyle,
) {
Markdown(
content = content,
@@ -4,7 +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 nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object GlobalFeedFilter: FeedFilter<Note>() {
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.Note
import com.vitorpamplona.amethyst.service.model.RepostEvent
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object HomeConversationsFeedFilter: FeedFilter<Note>() {
lateinit var account: Account
@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object HomeNewThreadFeedFilter: FeedFilter<Note>() {
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.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NotificationFeedFilter: FeedFilter<Note>() {
lateinit var account: Account
@@ -15,7 +15,7 @@ object UserProfileNewThreadFeedFilter: FeedFilter<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() }
?.sortedBy { it.event?.createdAt }
?.reversed()
@@ -122,7 +122,7 @@ private fun messagesHasNewItems(account: Account, cache: NotificationCache, cont
ChatroomListKnownFeedFilter.account = account
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
val lastTime = cache.load("Room/${note.author?.pubkeyHex}", context)
@@ -72,7 +72,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
launchSingleTop = true
}
} else {
note.channel?.let {
note.channel()?.let {
navController.navigate("Channel/${it.idHex}")
}
}
@@ -63,11 +63,11 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
if (note?.event == null) {
BlankNote(Modifier)
} else if (note.channel != null) {
} else if (note.channel() != null) {
val authorState by note.author!!.live().metadata.observeAsState()
val author = authorState?.user
val channelState by note.channel!!.live.observeAsState()
val channelState by note.channel()!!.live.observeAsState()
val channel = channelState?.channel
val noteEvent = note.event
@@ -72,7 +72,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn
launchSingleTop = true
}
} else {
note.channel?.let {
note.channel()?.let {
navController.navigate("Channel/${it.idHex}")
}
}
@@ -78,7 +78,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, modifier: Modifier = Modifier, r
launchSingleTop = true
}
} else {
note.channel?.let {
note.channel()?.let {
navController.navigate("Channel/${it.idHex}")
}
}
@@ -35,6 +35,7 @@ import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
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 kotlin.time.ExperimentalTime
import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.TextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -133,7 +134,7 @@ fun NoteCompose(
launchSingleTop = true
}
} else {
note.channel?.let {
note.channel()?.let {
navController.navigate("Channel/${it.idHex}")
}
}
@@ -175,7 +176,7 @@ fun NoteCompose(
}
// boosted picture
val baseChannel = note.channel
val baseChannel = note.channel()
if (noteEvent is ChannelMessageEvent && baseChannel != null) {
val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel
@@ -293,7 +294,7 @@ fun NoteCompose(
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || note.mentions != null)) {
val sortedMentions = note.mentions?.toSet()?.sortedBy { account.userProfile().isFollowing(it) }
note.channel?.let {
note.channel()?.let {
ReplyInformationChannel(note.replyTo, sortedMentions, it, navController)
}
}
@@ -74,7 +74,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInner
launchSingleTop = true
}
} else {
note.channel?.let {
note.channel()?.let {
navController.navigate("Channel/${it.idHex}")
}
}
@@ -268,7 +268,7 @@ fun NoteMaster(baseNote: Note,
}
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 {
noteEvent.image?.let {
AsyncImage(
@@ -295,7 +295,10 @@ fun NoteMaster(baseNote: Note,
noteEvent.summary?.let {
Text(
text = it
text = it,
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp)
)
}
}
@@ -193,7 +193,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
trailingIcon = {
PostButton(
onPost = {
account.sendChannelMeesage(newPost.value.text, channel.idHex, replyTo.value, null)
account.sendChannelMessage(newPost.value.text, channel.idHex, replyTo.value, null)
newPost.value = TextFieldValue("")
replyTo.value = null
feedViewModel.refresh() // Don't wait a full second before updating