Early support for Markdown and Long Form Text
This commit is contained in:
@@ -130,6 +130,11 @@ dependencies {
|
|||||||
implementation 'androidx.camera:camera-lifecycle:1.2.1'
|
implementation 'androidx.camera:camera-lifecycle:1.2.1'
|
||||||
implementation 'androidx.camera:camera-view:1.2.1'
|
implementation 'androidx.camera:camera-view:1.2.1'
|
||||||
|
|
||||||
|
// Markdown
|
||||||
|
implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
|
||||||
|
implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0"
|
||||||
|
implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
|
||||||
|
|
||||||
// For QR Scanning
|
// For QR Scanning
|
||||||
implementation 'com.google.mlkit:vision-common:17.3.0'
|
implementation 'com.google.mlkit:vision-common:17.3.0'
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||||
|
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.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
@@ -184,7 +185,52 @@ object LocalCache {
|
|||||||
refreshObservers()
|
refreshObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> {
|
fun consume(event: LongTextNoteEvent, relay: Relay?) {
|
||||||
|
if (antiSpam.isSpam(event)) {
|
||||||
|
relay?.let {
|
||||||
|
it.spamCounter++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val note = getOrCreateNote(event.id.toHex())
|
||||||
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
|
||||||
|
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
|
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)}")
|
||||||
|
|
||||||
|
// Prepares user's profile view.
|
||||||
|
author.addNote(note)
|
||||||
|
|
||||||
|
// Adds notifications to users.
|
||||||
|
mentions.forEach {
|
||||||
|
it.addTaggedPost(note)
|
||||||
|
}
|
||||||
|
replyTo.forEach {
|
||||||
|
it.author?.addTaggedPost(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
replyTo.forEach {
|
||||||
|
it.addReply(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findCitations(event: Event): Set<String> {
|
||||||
var citations = mutableSetOf<String>()
|
var citations = mutableSetOf<String>()
|
||||||
// Removes citations from replies:
|
// Removes citations from replies:
|
||||||
val matcher = tagSearch.matcher(event.content)
|
val matcher = tagSearch.matcher(event.content)
|
||||||
@@ -198,6 +244,17 @@ object LocalCache {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return citations
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> {
|
||||||
|
val citations = findCitations(event)
|
||||||
|
|
||||||
|
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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||||
|
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.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
@@ -90,6 +91,8 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
ChannelMessageEvent.kind -> LocalCache.consume(ChannelMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
ChannelMessageEvent.kind -> LocalCache.consume(ChannelMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
||||||
ChannelHideMessageEvent.kind -> LocalCache.consume(ChannelHideMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
ChannelHideMessageEvent.kind -> LocalCache.consume(ChannelHideMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
||||||
ChannelMuteUserEvent.kind -> LocalCache.consume(ChannelMuteUserEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
ChannelMuteUserEvent.kind -> LocalCache.consume(ChannelMuteUserEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
||||||
|
|
||||||
|
LongTextNoteEvent.kind -> LocalCache.consume(LongTextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.UserState
|
import com.vitorpamplona.amethyst.model.UserState
|
||||||
|
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 kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -47,7 +48,7 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") {
|
|||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = setOf(FeedType.FOLLOWS),
|
types = setOf(FeedType.FOLLOWS),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind),
|
||||||
authors = followSet,
|
authors = followSet,
|
||||||
limit = 400
|
limit = 400
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
import nostr.postr.Utils
|
||||||
|
import nostr.postr.events.Event
|
||||||
|
|
||||||
|
class LongTextNoteEvent(
|
||||||
|
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 title: String?
|
||||||
|
@Transient val image: String?
|
||||||
|
@Transient val summary: String?
|
||||||
|
@Transient val publishedAt: Long?
|
||||||
|
@Transient val topics: List<String>
|
||||||
|
|
||||||
|
init {
|
||||||
|
replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
mentions = tags.filter { it.firstOrNull() == "p" }.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()
|
||||||
|
image = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
summary = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
publishedAt = try {
|
||||||
|
tags.filter { it.firstOrNull() == "published_at" }.mapNotNull { it.getOrNull(1) }.firstOrNull()?.toLong()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 30023
|
||||||
|
|
||||||
|
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent {
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||||
|
val tags = mutableListOf<List<String>>()
|
||||||
|
replyTos?.forEach {
|
||||||
|
tags.add(listOf("e", it))
|
||||||
|
}
|
||||||
|
mentions?.forEach {
|
||||||
|
tags.add(listOf("p", it))
|
||||||
|
}
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,24 +2,44 @@ package com.vitorpamplona.amethyst.ui.components
|
|||||||
|
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.LocalContentColor
|
||||||
import androidx.compose.material.LocalTextStyle
|
import androidx.compose.material.LocalTextStyle
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.contentColorFor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.compositeOver
|
import androidx.compose.ui.graphics.compositeOver
|
||||||
|
import androidx.compose.ui.text.Paragraph
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.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.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.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.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
|
||||||
@@ -60,61 +80,82 @@ fun RichTextViewer(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Column(modifier = modifier.animateContentSize()) {
|
Column(modifier = modifier.animateContentSize()) {
|
||||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
|
||||||
content.split('\n').forEach { paragraph ->
|
|
||||||
|
|
||||||
FlowRow() {
|
if (content.startsWith("# ") || content.contains("##") || content.contains("```")) {
|
||||||
paragraph.split(' ').forEach { word: String ->
|
var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) }
|
||||||
|
|
||||||
if (canPreview) {
|
MaterialRichText(
|
||||||
// Explicit URL
|
style = RichTextStyle().resolveDefaults().copy(
|
||||||
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
stringStyle = richTextStyle.stringStyle?.copy(
|
||||||
if (lnInvoice != null) {
|
linkStyle = SpanStyle(
|
||||||
InvoicePreview(lnInvoice)
|
textDecoration = TextDecoration.Underline,
|
||||||
} else if (isValidURL(word)) {
|
color = MaterialTheme.colors.primary
|
||||||
val removedParamsFromUrl = word.split("?")[0].toLowerCase()
|
)
|
||||||
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
)
|
||||||
ZoomableImageView(word)
|
),
|
||||||
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
) {
|
||||||
VideoView(word)
|
Markdown(
|
||||||
|
content = content,
|
||||||
|
markdownParseOptions = MarkdownParseOptions.Default,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||||
|
content.split('\n').forEach { paragraph ->
|
||||||
|
FlowRow() {
|
||||||
|
paragraph.split(' ').forEach { word: String ->
|
||||||
|
|
||||||
|
if (canPreview) {
|
||||||
|
// Explicit URL
|
||||||
|
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
||||||
|
if (lnInvoice != null) {
|
||||||
|
InvoicePreview(lnInvoice)
|
||||||
|
} else if (isValidURL(word)) {
|
||||||
|
val removedParamsFromUrl = word.split("?")[0].toLowerCase()
|
||||||
|
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
|
ZoomableImageView(word)
|
||||||
|
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
|
VideoView(word)
|
||||||
|
} else {
|
||||||
|
UrlPreview(word, word)
|
||||||
|
}
|
||||||
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||||
|
ClickableEmail(word)
|
||||||
|
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||||
|
ClickablePhone(word)
|
||||||
|
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||||
|
UrlPreview("https://$word", word)
|
||||||
|
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
||||||
|
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
||||||
|
} else if (isBechLink(word)) {
|
||||||
|
BechLink(word, navController)
|
||||||
} else {
|
} else {
|
||||||
UrlPreview(word, word)
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
|
||||||
ClickableEmail(word)
|
|
||||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
|
||||||
ClickablePhone(word)
|
|
||||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
|
||||||
UrlPreview("https://$word", word)
|
|
||||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
|
||||||
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
|
||||||
} else if (isBechLink(word)) {
|
|
||||||
BechLink(word, navController)
|
|
||||||
} else {
|
} else {
|
||||||
Text(
|
if (isValidURL(word)) {
|
||||||
text = "$word ",
|
ClickableUrl("$word ", word)
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||||
)
|
ClickableEmail(word)
|
||||||
}
|
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||||
} else {
|
ClickablePhone(word)
|
||||||
if (isValidURL(word)) {
|
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||||
ClickableUrl("$word ", word)
|
ClickableUrl(word, "https://$word")
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
||||||
ClickableEmail(word)
|
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
||||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
} else if (isBechLink(word)) {
|
||||||
ClickablePhone(word)
|
BechLink(word, navController)
|
||||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
} else {
|
||||||
ClickableUrl(word, "https://$word")
|
Text(
|
||||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
text = "$word ",
|
||||||
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
} else if (isBechLink(word)) {
|
)
|
||||||
BechLink(word, navController)
|
}
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "$word ",
|
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
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.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
|
|||||||
|
|
||||||
return LocalCache.notes.values
|
return LocalCache.notes.values
|
||||||
.filter {
|
.filter {
|
||||||
(it.event is TextNoteEvent || it.event is RepostEvent)
|
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
|
||||||
&& it.author in user.follows
|
&& it.author in user.follows
|
||||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||||
&& it.author?.let { !account.isHidden(it) } ?: true
|
&& it.author?.let { !account.isHidden(it) } ?: true
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import androidx.compose.ui.graphics.ColorMatrix
|
import androidx.compose.ui.graphics.ColorMatrix
|
||||||
import androidx.compose.ui.graphics.compositeOver
|
import androidx.compose.ui.graphics.compositeOver
|
||||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
@@ -25,6 +26,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -36,6 +38,7 @@ import com.vitorpamplona.amethyst.RoboHashCache
|
|||||||
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
|
||||||
|
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.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
@@ -43,6 +46,7 @@ import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
|
|||||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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
|
||||||
@@ -337,6 +341,58 @@ fun NoteCompose(
|
|||||||
modifier = Modifier.padding(top = 40.dp),
|
modifier = Modifier.padding(top = 40.dp),
|
||||||
thickness = 0.25.dp
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactionsRow(note, accountViewModel)
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(top = 10.dp),
|
||||||
|
thickness = 0.25.dp
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
val eventContent = accountViewModel.decrypt(note)
|
val eventContent = accountViewModel.decrypt(note)
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.draw.drawBehind
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.compositeOver
|
import androidx.compose.ui.graphics.compositeOver
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||||
@@ -51,6 +52,13 @@ import com.vitorpamplona.amethyst.ui.note.timeAgo
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
|
fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
@@ -259,6 +267,41 @@ fun NoteMaster(baseNote: Note,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (noteEvent is LongTextNoteEvent) {
|
||||||
|
Row(modifier = Modifier.padding(horizontal = 12.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,
|
||||||
|
fontSize = 30.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
noteEvent.summary?.let {
|
||||||
|
Text(
|
||||||
|
text = it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(modifier = Modifier.padding(horizontal = 12.dp)) {
|
Row(modifier = Modifier.padding(horizontal = 12.dp)) {
|
||||||
Column() {
|
Column() {
|
||||||
val eventContent = note.event?.content
|
val eventContent = note.event?.content
|
||||||
|
|||||||
Reference in New Issue
Block a user