Improve RichContent for reposted notes

This commit is contained in:
Kgothatso Ngako committed 2026-05-01 19:07:55 +02:00
1 parent 54f6a0ee26
commit 2a865698cb
7 files changed
+407 -177

No files matched your search

@@ -0,0 +1,101 @@
package ac.cord.auxiliary.compose.database.model.intermdiate
import ac.cord.auxiliary.compose.database.model.InReplyToRelation
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.QuotedRelation
import ac.cord.auxiliary.compose.extensions.toFormattedTimeAndDateString
import ac.cord.auxiliary.compose.ui.composable.widgets.profile.ProfileAvatar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.room.Embedded
import androidx.room.Relation
data class LocalInReplyToNostrEvent(
@Embedded val inReplyToRelation: InReplyToRelation,
@Relation(
parentColumn = "inReplyToNostrEventId",
entityColumn = "id"
)
val nostrEvent: NostrEvent,
@Relation(
parentColumn = "inReplyToProfilePublicKey",
entityColumn = "publicKey"
)
val profile: Profile?,
// TODO: Figure out if we want to add the root of the reply...
// @Relation(
// entity = Mention::class,
// parentColumn = "id",
// entityColumn = "nostrEventId",
// )
// val mentionedProfiles: List<LocalMention?> = emptyList(),
) {
@Composable
fun RenderNotePreview() {
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
val profile = profile
ProfileAvatar(
profile = profile,
publicKey = nostrEvent.pubKey
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = profile?.displayName ?: profile?.userName ?: nostrEvent.pubKey,
style = MaterialTheme.typography.labelLarge
)
profile?.nip05?.let {
Text(
text = profile.nip05 ?: "",
style = MaterialTheme.typography.labelMedium
)
}
}
}
// TODO: Use RichContent...
Text(
modifier = Modifier.fillMaxWidth().padding(
horizontal = 20.dp
),
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Start,
overflow = TextOverflow.Ellipsis,
text = nostrEvent.content
)
Row(
modifier = Modifier.fillMaxWidth().padding(20.dp),
horizontalArrangement = Arrangement.Start
) {
Text(
text = nostrEvent.createdAt.toFormattedTimeAndDateString(),
style = MaterialTheme.typography.labelSmall
)
}
}
}
}
@@ -36,10 +36,11 @@ data class LocalNostrEvent(
val profile: Profile?,
@Relation(
entity = RepostedRelation::class,
parentColumn = "id",
entityColumn = "repostingNostrEventId"
)
val repostedRelation: RepostedRelation? = null,
val localRepostedNostrEvent: LocalRepostedNostrEvent? = null,
@Relation(
entity = QuotedRelation::class,
@@ -48,33 +49,12 @@ data class LocalNostrEvent(
)
val localQuotedNostrEvent: LocalQuotedNostrEvent? = null,
// @Relation(
// parentColumn = "id",
// entityColumn = "id",
// associateBy = Junction(
// value = QuotedRelation::class,
// parentColumn = "quotingNostrEventId",
// entityColumn = "quotedNostrEventId"
// )
// )
// val quotedNostrEvent: NostrEvent? = null,
@Relation(
parentColumn = "id",
entityColumn = "publicKey",
associateBy = Junction(
value = QuotedRelation::class,
parentColumn = "quotingNostrEventId",
entityColumn = "quotedProfilePublicKey"
)
)
val quotedAuthorProfile: Profile? = null,
@Relation(
entity = InReplyToRelation::class,
parentColumn = "id",
entityColumn = "replyingNostrEventId"
)
val inReplyToNostrEvent: InReplyToRelation? = null,
val localInReplyToNostrEvent: LocalInReplyToNostrEvent? = null,
@Relation(
entity = Mention::class,
@@ -0,0 +1,100 @@
package ac.cord.auxiliary.compose.database.model.intermdiate
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.QuotedRelation
import ac.cord.auxiliary.compose.database.model.RepostedRelation
import ac.cord.auxiliary.compose.extensions.toFormattedTimeAndDateString
import ac.cord.auxiliary.compose.ui.composable.widgets.profile.ProfileAvatar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.room.Embedded
import androidx.room.Relation
data class LocalRepostedNostrEvent(
@Embedded val repostedRelation: RepostedRelation,
@Relation(
parentColumn = "repostedNostrEventId",
entityColumn = "id"
)
val nostrEvent: NostrEvent,
@Relation(
parentColumn = "repostedProfilePublicKey",
entityColumn = "publicKey"
)
val profile: Profile?,
// @Relation(
// entity = Mention::class,
// parentColumn = "id",
// entityColumn = "nostrEventId",
// )
// val mentionedProfiles: List<LocalMention?> = emptyList(),
) {
@Composable
fun RenderNotePreview() {
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
val profile = profile
ProfileAvatar(
profile = profile,
publicKey = nostrEvent.pubKey
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = profile?.displayName ?: profile?.userName ?: nostrEvent.pubKey,
style = MaterialTheme.typography.labelLarge
)
profile?.nip05?.let {
Text(
text = profile.nip05 ?: "",
style = MaterialTheme.typography.labelMedium
)
}
}
}
// TODO: Use RichContent...
Text(
modifier = Modifier.fillMaxWidth().padding(
horizontal = 20.dp
),
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Start,
overflow = TextOverflow.Ellipsis,
text = nostrEvent.content
)
Row(
modifier = Modifier.fillMaxWidth().padding(20.dp),
horizontalArrangement = Arrangement.Start
) {
Text(
text = nostrEvent.createdAt.toFormattedTimeAndDateString(),
style = MaterialTheme.typography.labelSmall
)
}
}
}
}
@@ -1,6 +1,9 @@
package ac.cord.auxiliary.compose.ui.composable.widgets.content
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalMention
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalQuotedNostrEvent
import ac.cord.auxiliary.compose.ui.composable.widgets.content.Constants.INLINE_CONTENT_TAG
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -30,7 +33,9 @@ import coil3.compose.AsyncImage
@Composable
fun RichContent(
localNostrEvent: LocalNostrEvent,
nostrEvent: NostrEvent,
localQuotedNostrEvent: LocalQuotedNostrEvent?,
mentionedProfiles: List<LocalMention?>,
style: TextStyle = MaterialTheme.typography.bodyLarge,
color: Color = MaterialTheme.colorScheme.onSurface,
linkColor: Color = Color.Unspecified,
@@ -44,7 +49,7 @@ fun RichContent(
noteActions: NoteActions? = null,
modifier: Modifier = Modifier
) {
val content = localNostrEvent.nostrEvent.content
val content = nostrEvent.content
val segments = remember(content, emojiMap, imetaMap, plainLinks) { ContentParser.parse(content.trimEnd('\n', '\r'), emojiMap, imetaMap, trimBlankLines = !plainLinks) }
// val profileVer = eventRepo?.profileVersion?.collectAsState()?.value ?: 0
@@ -105,10 +110,10 @@ fun RichContent(
val profileNames = remember(profilePubkeys) {
val names = mutableMapOf<String, String>()
if (localNostrEvent.mentionedProfiles.isNotEmpty()) {
Logger.withTag("RichContent").d("mentionedProfiles: ${localNostrEvent.mentionedProfiles}")
if (mentionedProfiles.isNotEmpty()) {
Logger.withTag("RichContent").d("mentionedProfiles: ${mentionedProfiles}")
for (pubkey in profilePubkeys) {
val mentionedProfile = localNostrEvent.mentionedProfiles.find { mentionedProfile -> mentionedProfile?.profile?.publicKey == pubkey }
val mentionedProfile = mentionedProfiles.find { mentionedProfile -> mentionedProfile?.profile?.publicKey == pubkey }
names[pubkey] = mentionedProfile?.profile?.humanReadableNameOrPubkey()
?: "${pubkey.take(8)}...${pubkey.takeLast(4)}"
}
@@ -275,9 +280,9 @@ fun RichContent(
LinkPreview(url = segment.url)
}
is ContentSegment.NostrNoteSegment -> {
if (localNostrEvent.localQuotedNostrEvent != null) {
if (localQuotedNostrEvent != null) {
QuotedNote(
localQuotedNostrEvent = localNostrEvent.localQuotedNostrEvent,
localQuotedNostrEvent = localQuotedNostrEvent,
relayHints = segment.relayHints,
onNoteClick = onNoteClick,
noteActions = noteActions
@@ -141,7 +141,9 @@ fun TextNoteEventDetail(
modifier = Modifier.fillMaxWidth().padding(
horizontal = 20.dp
),
localNostrEvent = localNostrEvent,
nostrEvent = localNostrEvent.nostrEvent,
localQuotedNostrEvent = localNostrEvent.localQuotedNostrEvent,
mentionedProfiles = localNostrEvent.mentionedProfiles
)
Row(
@@ -62,9 +62,13 @@ fun LocalNostrEvent.ListView(
style = MaterialTheme.typography.labelSmall)
}
if (repostedRelation != null) {
if (localRepostedNostrEvent != null) {
TextNoteFeedItem(
localNostrEvent = this@ListView,
nostrEvent = localRepostedNostrEvent.nostrEvent,
profile = localRepostedNostrEvent.profile,
localQuotedNostrEvent = null,
localInReplyToNostrEvent = null,
mentionedProfiles = emptyList(), // TODO: Add localReposted.mentionedProfiles...
onNavigateToEvent = onNavigateToEvent
)
} else {
@@ -96,155 +100,17 @@ fun LocalNostrEvent.ListView(
// TODO: Support other kinds...
else -> {
TextNoteFeedItem(
localNostrEvent = this,
nostrEvent = nostrEvent,
profile = profile,
localQuotedNostrEvent = localQuotedNostrEvent,
localInReplyToNostrEvent = localInReplyToNostrEvent,
mentionedProfiles = mentionedProfiles,
onNavigateToEvent = onNavigateToEvent
)
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun TextNoteFeedItem(
localNostrEvent: LocalNostrEvent,
onNavigateToEvent: (HexKey) -> Unit
) {
val nostrEvent = localNostrEvent.nostrEvent
if (localNostrEvent.repostedRelation != null) {
Row(
modifier = Modifier.fillMaxWidth().padding(
horizontal = 20.dp
),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
// TODO: Get a nested repost... localNostrEvent...
Text(
"In reply to ",
style = MaterialTheme.typography.labelSmall
)
Text(
modifier = Modifier.clickable(
onClick = {
onNavigateToEvent.invoke(localNostrEvent.repostedRelation.repostedNostrEventId)
}
),
style = MaterialTheme.typography.labelLargeEmphasized,
text = localNostrEvent.repostedRelation.repostedProfilePublicKey ?: "loading information..."
)
}
}
ElevatedCard(
modifier = Modifier.padding(
horizontal = 10.dp
),
onClick = {
onNavigateToEvent.invoke(nostrEvent.id)
}
) {
Column(
modifier = Modifier.padding(10.dp).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val profile = localNostrEvent.profile
if (profile != null) {
Row(
modifier = Modifier.fillMaxWidth().clickable(
onClick = {
onNavigateToEvent.invoke(profile.nostrEventId)
}
),
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically
) {
ProfileAvatar(
publicKey = nostrEvent.pubKey,
profile = profile
)
Text(
modifier = Modifier.weight(1f),
text = profile.displayName ?: profile.userName ?: nostrEvent.pubKey,
style = MaterialTheme.typography.bodyMedium
)
}
} else {
Text(
text = "Loading author information"
)
}
RichContent(
modifier = Modifier.fillMaxWidth(),
localNostrEvent = localNostrEvent,
)
LazyRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
10.dp,
alignment = Alignment.Start
)
) {
item {
AssistChip(
label = {
Text(
text = nostrEvent.kind.toString() // TODO: Kind by name...
)
},
onClick = {
}
)
}
// item {
// SuggestionChip(
// label = {
// Text(
// text = if (localNostrEvent.mentionedProfiles.isNotEmpty()) {
// localNostrEvent.mentionedProfiles.joinToString(",") { it?.profile?.humanReadableNameOrPubkey() ?: "Find user information" }
// } else {
// ":-("
// }
// )
// },
// onClick = {
//
// }
// )
// }
items(
items = nostrEvent.tags,
) { tag ->
SuggestionChip(
label = {
Text(
text = tag.joinToString(":"),
maxLines = 1
)
},
onClick = {
}
)
}
}
Text(
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.End,
text = nostrEvent.createdAt.toFormattedTimeAndDateString()
)
}
}
}
@Preview
@Composable
@@ -0,0 +1,176 @@
package ac.cord.auxiliary.compose.ui.composable.widgets.feed
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalInReplyToNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalMention
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalQuotedNostrEvent
import ac.cord.auxiliary.compose.extensions.toFormattedTimeAndDateString
import ac.cord.auxiliary.compose.ui.composable.widgets.content.RichContent
import ac.cord.auxiliary.compose.ui.composable.widgets.profile.ProfileAvatar
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AssistChip
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
internal fun TextNoteFeedItem(
nostrEvent: NostrEvent,
profile: Profile?,
localInReplyToNostrEvent: LocalInReplyToNostrEvent?,
localQuotedNostrEvent: LocalQuotedNostrEvent?,
mentionedProfiles: List<LocalMention?>,
onNavigateToEvent: (HexKey) -> Unit
) {
if (localInReplyToNostrEvent != null) {
Row(
modifier = Modifier.fillMaxWidth().padding(
horizontal = 20.dp
),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"In reply to ",
style = MaterialTheme.typography.labelSmall
)
Text(
modifier = Modifier.clickable(
onClick = {
onNavigateToEvent.invoke(localInReplyToNostrEvent.inReplyToRelation.inReplyToNostrEventId)
}
),
style = MaterialTheme.typography.labelLargeEmphasized,
text = localInReplyToNostrEvent.profile?.humanReadableNameOrPubkey() ?: "loading information..."
)
}
}
ElevatedCard(
modifier = Modifier.padding(
horizontal = 10.dp
),
onClick = {
onNavigateToEvent.invoke(nostrEvent.id)
}
) {
Column(
modifier = Modifier.padding(10.dp).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (profile != null) {
Row(
modifier = Modifier.fillMaxWidth().clickable(
onClick = {
onNavigateToEvent.invoke(profile.nostrEventId)
}
),
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically
) {
ProfileAvatar(
publicKey = nostrEvent.pubKey,
profile = profile
)
Text(
modifier = Modifier.weight(1f),
text = profile.displayName ?: profile.userName ?: nostrEvent.pubKey,
style = MaterialTheme.typography.bodyMedium
)
}
} else {
Text(
text = "Loading author information"
)
}
RichContent(
modifier = Modifier.fillMaxWidth(),
nostrEvent = nostrEvent,
localQuotedNostrEvent = localQuotedNostrEvent,
mentionedProfiles = mentionedProfiles
)
LazyRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
10.dp,
alignment = Alignment.Start
)
) {
item {
AssistChip(
label = {
Text(
text = nostrEvent.kind.toString() // TODO: Kind by name...
)
},
onClick = {
}
)
}
// item {
// SuggestionChip(
// label = {
// Text(
// text = if (localNostrEvent.mentionedProfiles.isNotEmpty()) {
// localNostrEvent.mentionedProfiles.joinToString(",") { it?.profile?.humanReadableNameOrPubkey() ?: "Find user information" }
// } else {
// ":-("
// }
// )
// },
// onClick = {
//
// }
// )
// }
items(
items = nostrEvent.tags,
) { tag ->
SuggestionChip(
label = {
Text(
text = tag.joinToString(":"),
maxLines = 1
)
},
onClick = {
}
)
}
}
Text(
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.End,
text = nostrEvent.createdAt.toFormattedTimeAndDateString()
)
}
}
}