Improve RichContent for reposted notes

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

View File

@@ -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
)
}
}
}
}

View File

@@ -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,

View File

@@ -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
)
}
}
}
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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()
)
}
}
}