diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalInReplyToNostrEvent.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalInReplyToNostrEvent.kt new file mode 100644 index 00000000..a10d41f0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalInReplyToNostrEvent.kt @@ -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 = 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 + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalNostrEvent.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalNostrEvent.kt index 8f1dcfce..0d6a21b3 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalNostrEvent.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalNostrEvent.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalRepostedNostrEvent.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalRepostedNostrEvent.kt new file mode 100644 index 00000000..e7355e42 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalRepostedNostrEvent.kt @@ -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 = 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 + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/content/RichContent.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/content/RichContent.kt index 258fbeab..262039db 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/content/RichContent.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/content/RichContent.kt @@ -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, 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() - 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 diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/TextNoteEventDetail.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/TextNoteEventDetail.kt index bc67037f..1872eff5 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/TextNoteEventDetail.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/TextNoteEventDetail.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/feed/EventListView.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/feed/EventListView.kt index 58fd4bdf..96aae26a 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/feed/EventListView.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/feed/EventListView.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/feed/TextNoteFeedItem.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/feed/TextNoteFeedItem.kt new file mode 100644 index 00000000..4e2243c9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/feed/TextNoteFeedItem.kt @@ -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, + 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() + ) + } + } +}