Skip to content

Commit

Permalink
Implement saving highlights from note feeds (#248)
Browse files Browse the repository at this point in the history
* Implement Highlight db entities;
* Store highlights from note feeds;
* Upgrade to compose-richtext:1.0.0-alpha02;
* Implement custom block composer of AstParagraph nodes;
  • Loading branch information
markocic authored Dec 10, 2024
1 parent d889988 commit 3475969
Show file tree
Hide file tree
Showing 19 changed files with 2,349 additions and 10 deletions.
1,863 changes: 1,863 additions & 0 deletions app/schemas/net.primal.android.db.PrimalDatabase/50.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions app/src/main/kotlin/net/primal/android/articles/db/Article.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.primal.android.articles.db
import androidx.room.Embedded
import androidx.room.Relation
import net.primal.android.bookmarks.db.PublicBookmark
import net.primal.android.highlights.db.HighlightData
import net.primal.android.profile.db.ProfileData
import net.primal.android.stats.db.EventStats
import net.primal.android.stats.db.EventUserStats
Expand All @@ -26,4 +27,7 @@ data class Article(

@Relation(entityColumn = "tagValue", parentColumn = "aTag")
val bookmark: PublicBookmark? = null,

@Relation(entityColumn = "referencedEventId", parentColumn = "aTag")
val highlights: List<HighlightData> = emptyList(),
)
7 changes: 6 additions & 1 deletion app/src/main/kotlin/net/primal/android/db/PrimalDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import net.primal.android.explore.db.TrendingTopic
import net.primal.android.explore.db.TrendingTopicDao
import net.primal.android.feeds.db.Feed
import net.primal.android.feeds.db.FeedDao
import net.primal.android.highlights.db.HighlightDao
import net.primal.android.highlights.db.HighlightData
import net.primal.android.messages.db.DirectMessageDao
import net.primal.android.messages.db.DirectMessageData
import net.primal.android.messages.db.MessageConversationDao
Expand Down Expand Up @@ -89,8 +91,9 @@ import net.primal.android.wallet.db.WalletTransactionData
ArticleData::class,
ArticleCommentCrossRef::class,
ArticleFeedCrossRef::class,
HighlightData::class,
],
version = 49,
version = 50,
exportSchema = true,
)
@TypeConverters(
Expand Down Expand Up @@ -150,4 +153,6 @@ abstract class PrimalDatabase : RoomDatabase() {
abstract fun articles(): ArticleDao

abstract fun articleFeedsConnections(): ArticleFeedCrossRefDao

abstract fun highlights(): HighlightDao
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.primal.android.highlights.db

import androidx.room.Dao
import androidx.room.Upsert

@Dao
interface HighlightDao {
@Upsert
fun upsertAll(data: List<HighlightData>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package net.primal.android.highlights.db

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class HighlightData(
@PrimaryKey
val highlightId: String,
val authorId: String,
val content: String,
val context: String?,
val alt: String?,
val referencedEventId: String?,
val referencedEventAuthorId: String?,
val createdAt: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package net.primal.android.highlights.model

import net.primal.android.highlights.db.HighlightData

data class HighlightUi(
val highlightId: String,
val authorId: String,
val content: String,
val context: String?,
val alt: String?,
val highlightEventId: String?,
val highlightEventAuthorId: String?,
val createdAt: Long,
)

fun HighlightData.asHighlightUi() =
HighlightUi(
highlightId = highlightId,
authorId = authorId,
content = content,
context = context,
alt = alt,
highlightEventId = referencedEventId,
highlightEventAuthorId = referencedEventAuthorId,
createdAt = createdAt,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package net.primal.android.nostr.ext

import net.primal.android.highlights.db.HighlightData
import net.primal.android.nostr.model.NostrEvent
import net.primal.android.nostr.model.NostrEventKind
import net.primal.android.nostr.model.primal.PrimalEvent

fun List<PrimalEvent>.mapReferencedEventsAsHighlightDataPO() =
this.mapNotNull { it.takeContentOrNull<NostrEvent>() }
.filter { it.kind == NostrEventKind.Highlight.value }
.map { it.asHighlightData() }

fun NostrEvent.asHighlightData() =
HighlightData(
highlightId = this.id,
authorId = this.pubKey,
content = this.content,
alt = this.tags.findFirstAltDescription(),
context = this.tags.findFirstContextTag(),
referencedEventId = this.tags.findFirstReplaceableEventId(),
referencedEventAuthorId = this.tags.findFirstProfileId()?.extractProfileId(),
createdAt = this.createdAt,
)
6 changes: 6 additions & 0 deletions app/src/main/kotlin/net/primal/android/nostr/ext/Tags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ fun List<JsonArray>.findFirstIdentifier() = firstOrNull { it.isIdentifierTag() }

fun List<JsonArray>.findFirstAltDescription() = firstOrNull { it.isAltTag() }?.getTagValueOrNull()

fun List<JsonArray>.findFirstReplaceableEventId() = firstOrNull { it.isATag() }?.getTagValueOrNull()

fun List<JsonArray>.findFirstContextTag() = firstOrNull { it.isContextTag() }?.getTagValueOrNull()

fun JsonArray.isContextTag() = getOrNull(0)?.jsonPrimitive?.content == "context"

fun JsonArray.isBolt11Tag() = getOrNull(0)?.jsonPrimitive?.content == "bolt11"

fun JsonArray.isDescriptionTag() = getOrNull(0)?.jsonPrimitive?.content == "description"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import net.primal.android.nostr.ext.mapNotNullAsEventUserStatsPO
import net.primal.android.nostr.ext.mapNotNullAsPostDataPO
import net.primal.android.nostr.ext.mapNotNullAsRepostDataPO
import net.primal.android.nostr.ext.mapReferencedEventsAsArticleDataPO
import net.primal.android.nostr.ext.mapReferencedEventsAsHighlightDataPO
import net.primal.android.nostr.ext.parseAndMapPrimalLegendProfiles
import net.primal.android.nostr.ext.parseAndMapPrimalPremiumInfo
import net.primal.android.nostr.ext.parseAndMapPrimalUserNames
Expand All @@ -43,6 +44,7 @@ suspend fun FeedResponse.persistToDatabaseAsTransaction(userId: String, database

val articles = this.articles.mapNotNullAsArticleDataPO(cdnResources = cdnResources)
val referencedArticles = this.referencedEvents.mapReferencedEventsAsArticleDataPO(cdnResources = cdnResources)
val referencedHighlights = this.referencedEvents.mapReferencedEventsAsHighlightDataPO()
val allArticles = articles + referencedArticles

val primalUserNames = this.primalUserNames.parseAndMapPrimalUserNames()
Expand Down Expand Up @@ -95,6 +97,7 @@ suspend fun FeedResponse.persistToDatabaseAsTransaction(userId: String, database
database.eventStats().upsertAll(data = postStats)
database.eventUserStats().upsertAll(data = userPostStats)
database.articles().upsertAll(list = allArticles)
database.highlights().upsertAll(data = referencedHighlights)

val eventHintsDao = database.eventHints()
val hintsMap = eventHints.associateBy { it.eventId }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ private fun ArticleContentWithComments(
.fillMaxWidth()
.padding(all = 16.dp),
markdown = part.markdown,
highlights = state.article?.highlights ?: emptyList(),
onProfileClick = noteCallbacks.onProfileClick,
onNoteClick = noteCallbacks.onNoteClick,
onArticleClick = noteCallbacks.onArticleClick,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import net.primal.android.articles.db.Article
import net.primal.android.attachments.domain.CdnImage
import net.primal.android.core.utils.asEllipsizedNpub
import net.primal.android.core.utils.authorNameUiFriendly
import net.primal.android.highlights.model.HighlightUi
import net.primal.android.highlights.model.asHighlightUi
import net.primal.android.notes.feed.model.EventStatsUi
import net.primal.android.premium.legend.LegendaryCustomization
import net.primal.android.premium.legend.asLegendaryCustomization
Expand All @@ -28,6 +30,7 @@ data class ArticleDetailsUi(
val isBookmarked: Boolean = false,
val eventStatsUi: EventStatsUi = EventStatsUi(),
val authorLegendaryCustomization: LegendaryCustomization? = null,
val highlights: List<HighlightUi> = emptyList(),
)

fun Article.mapAsArticleDetailsUi(): ArticleDetailsUi {
Expand All @@ -50,5 +53,6 @@ fun Article.mapAsArticleDetailsUi(): ArticleDetailsUi {
isBookmarked = this.bookmark != null,
eventStatsUi = EventStatsUi.from(eventStats = this.eventStats, userStats = this.userEventStats),
authorLegendaryCustomization = this.author?.primalPremiumInfo?.legendProfile?.asLegendaryCustomization(),
highlights = this.highlights.map { it.asHighlightUi() },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.primal.android.thread.articles.details.ui.rendering

import androidx.compose.runtime.Composable
import com.halilibo.richtext.markdown.AstBlockNodeComposer
import com.halilibo.richtext.markdown.node.AstBlockNodeType
import com.halilibo.richtext.markdown.node.AstNode
import com.halilibo.richtext.markdown.node.AstParagraph
import com.halilibo.richtext.ui.RichTextScope
import net.primal.android.highlights.model.HighlightUi
import net.primal.android.thread.articles.details.ui.richtext.MarkdownRichText

fun customBlockNodeComposer(@Suppress("UnusedParameter") highlights: List<HighlightUi>) =
object : AstBlockNodeComposer {
override fun predicate(astBlockNodeType: AstBlockNodeType): Boolean =
when (astBlockNodeType) {
AstParagraph -> true
else -> false
}

@Composable
override fun RichTextScope.Compose(astNode: AstNode, visitChildren: @Composable (AstNode) -> Unit) {
require(astNode.type == AstParagraph)
MarkdownRichText(astNode = astNode)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.commonmark.MarkdownParseOptions
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.material3.RichText
import net.primal.android.highlights.model.HighlightUi
import net.primal.android.theme.AppTheme
import net.primal.android.thread.articles.details.ui.handleArticleLinkClick

@Composable
fun MarkdownRenderer(
markdown: String,
modifier: Modifier = Modifier,
highlights: List<HighlightUi> = emptyList(),
onProfileClick: ((profileId: String) -> Unit)? = null,
onNoteClick: ((noteId: String) -> Unit)? = null,
onArticleClick: ((naddr: String) -> Unit)? = null,
Expand All @@ -26,14 +28,12 @@ fun MarkdownRenderer(
codeBlockContent = AppTheme.colorScheme.onSurface,
outlineColor = AppTheme.colorScheme.outline,
)
val parser = remember(markdown) { CommonmarkAstNodeParser(MarkdownParseOptions.Default) }
val parser = remember(markdown) { CommonmarkAstNodeParser(CommonMarkdownParseOptions.Default) }
val astNode = remember(parser) { parser.parse(markdown) }

SelectionContainer {
PrimalMarkdownStylesProvider {
RichText(
modifier = modifier,
style = richTextStyle,
PrimalMarkdownUriHandlerProvider(
linkClickHandler = { url ->
url.handleArticleLinkClick(
onProfileClick = onProfileClick,
Expand All @@ -43,7 +43,15 @@ fun MarkdownRenderer(
)
},
) {
BasicMarkdown(astNode = astNode)
RichText(
modifier = modifier,
style = richTextStyle,
) {
BasicMarkdown(
astNode = astNode,
astBlockNodeComposer = customBlockNodeComposer(highlights = highlights),
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
Expand Down Expand Up @@ -106,8 +107,10 @@ fun buildPrimalRichTextStyle(
color = codeBlockContent,
background = codeBlockBackground,
),
linkStyle = primalMarkdownBodyTextStyle.toSpanStyle().copy(
color = highlightColor,
linkStyle = TextLinkStyles(
style = primalMarkdownBodyTextStyle.toSpanStyle().copy(
color = highlightColor,
),
),
),
paragraphSpacing = 20.sp,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package net.primal.android.thread.articles.details.ui.rendering

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler

@Composable
fun PrimalMarkdownUriHandlerProvider(linkClickHandler: (uri: String) -> Unit, content: @Composable () -> Unit) {
val uriHandler = remember {
object : UriHandler {
override fun openUri(uri: String) = linkClickHandler(uri)
}
}
CompositionLocalProvider(LocalUriHandler provides uriHandler, content)
}
Loading

0 comments on commit 3475969

Please sign in to comment.