Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement highlight activity bottom sheet #254

Merged
merged 12 commits into from
Dec 16, 2024
Merged
4 changes: 2 additions & 2 deletions app/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>ComplexCondition:NostrResources.kt$isNote() || isNoteUri() || isNEventUri() || isNEvent()</ID>
<ID>CyclomaticComplexMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalLayoutApi::class) @Composable private fun ArticleContentWithComments( state: ArticleDetailsContract.UiState, articleParts: List&lt;ArticlePartRender&gt;, listState: LazyListState = rememberLazyListState(), showHighlights: Boolean, paddingValues: PaddingValues, onArticleCommentClick: (naddr: String) -&gt; Unit, onArticleHashtagClick: (hashtag: String) -&gt; Unit, onZapOptionsClick: () -&gt; Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -&gt; Unit, onPostAction: ((FeedPostAction) -&gt; Unit)? = null, onPostLongPressAction: ((FeedPostAction) -&gt; Unit)? = null, onFollowUnfollowClick: (() -&gt; Unit)? = null, onUiError: ((UiError) -&gt; Unit)? = null, )</ID>
<ID>CyclomaticComplexMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalLayoutApi::class) @Composable private fun ArticleContentWithComments( state: ArticleDetailsContract.UiState, articleParts: List&lt;ArticlePartRender&gt;, listState: LazyListState = rememberLazyListState(), showHighlights: Boolean, paddingValues: PaddingValues, onArticleCommentClick: (naddr: String) -&gt; Unit, onArticleHashtagClick: (hashtag: String) -&gt; Unit, onZapOptionsClick: () -&gt; Unit, onHighlightClick: (String) -&gt; Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -&gt; Unit, onPostAction: ((FeedPostAction) -&gt; Unit)? = null, onPostLongPressAction: ((FeedPostAction) -&gt; Unit)? = null, onFollowUnfollowClick: (() -&gt; Unit)? = null, onUiError: ((UiError) -&gt; Unit)? = null, )</ID>
<ID>CyclomaticComplexMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ArticleDetailsScreen( detailsState: ArticleDetailsContract.UiState, articleState: ArticleContract.UiState, detailsEventPublisher: (UiEvent) -&gt; Unit, articleEventPublisher: (ArticleContract.UiEvent) -&gt; Unit, onArticleHashtagClick: (hashtag: String) -&gt; Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -&gt; Unit, onClose: () -&gt; Unit, )</ID>
<ID>CyclomaticComplexMethod:ChatScreen.kt$@Composable private fun ChatMessageListItem( chatMessage: ChatMessageUi, previousMessage: ChatMessageUi? = null, nextMessage: ChatMessageUi? = null, onUrlClick: (String) -&gt; Unit, noteCallbacks: NoteCallbacks, )</ID>
<ID>CyclomaticComplexMethod:CreateTransactionScreen.kt$@ExperimentalComposeUiApi @ExperimentalMaterial3Api @Composable fun CreateTransactionScreen( state: CreateTransactionContract.UiState, eventPublisher: (CreateTransactionContract.UiEvent) -&gt; Unit, onClose: () -&gt; Unit, )</ID>
Expand Down Expand Up @@ -33,7 +33,7 @@
<ID>CyclomaticComplexMethod:WalletDashboardScreen.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun WalletDashboardScreen( state: WalletDashboardContract.UiState, onPrimaryDestinationChanged: (PrimalTopLevelDestination) -&gt; Unit, onDrawerDestinationClick: (DrawerScreenDestination) -&gt; Unit, onDrawerQrCodeClick: () -&gt; Unit, onWalletActivateClick: () -&gt; Unit, onProfileClick: (String) -&gt; Unit, onTransactionClick: (String) -&gt; Unit, onSendClick: () -&gt; Unit, onScanClick: () -&gt; Unit, onReceiveClick: () -&gt; Unit, eventPublisher: (UiEvent) -&gt; Unit, )</ID>
<ID>CyclomaticComplexMethod:WalletTransactionsMediator.kt$WalletTransactionsMediator$override suspend fun load(loadType: LoadType, state: PagingState&lt;Int, WalletTransaction&gt;): MediatorResult</ID>
<ID>DestructuringDeclarationWithTooManyEntries:PrimalDrawer.kt$val (avatarRef, usernameRef, iconRef, identifierRef, statsRef) = createRefs()</ID>
<ID>LongMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalLayoutApi::class) @Composable private fun ArticleContentWithComments( state: ArticleDetailsContract.UiState, articleParts: List&lt;ArticlePartRender&gt;, listState: LazyListState = rememberLazyListState(), showHighlights: Boolean, paddingValues: PaddingValues, onArticleCommentClick: (naddr: String) -&gt; Unit, onArticleHashtagClick: (hashtag: String) -&gt; Unit, onZapOptionsClick: () -&gt; Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -&gt; Unit, onPostAction: ((FeedPostAction) -&gt; Unit)? = null, onPostLongPressAction: ((FeedPostAction) -&gt; Unit)? = null, onFollowUnfollowClick: (() -&gt; Unit)? = null, onUiError: ((UiError) -&gt; Unit)? = null, )</ID>
<ID>LongMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalLayoutApi::class) @Composable private fun ArticleContentWithComments( state: ArticleDetailsContract.UiState, articleParts: List&lt;ArticlePartRender&gt;, listState: LazyListState = rememberLazyListState(), showHighlights: Boolean, paddingValues: PaddingValues, onArticleCommentClick: (naddr: String) -&gt; Unit, onArticleHashtagClick: (hashtag: String) -&gt; Unit, onZapOptionsClick: () -&gt; Unit, onHighlightClick: (String) -&gt; Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -&gt; Unit, onPostAction: ((FeedPostAction) -&gt; Unit)? = null, onPostLongPressAction: ((FeedPostAction) -&gt; Unit)? = null, onFollowUnfollowClick: (() -&gt; Unit)? = null, onUiError: ((UiError) -&gt; Unit)? = null, )</ID>
<ID>LongMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ArticleDetailsScreen( detailsState: ArticleDetailsContract.UiState, articleState: ArticleContract.UiState, detailsEventPublisher: (UiEvent) -&gt; Unit, articleEventPublisher: (ArticleContract.UiEvent) -&gt; Unit, onArticleHashtagClick: (hashtag: String) -&gt; Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -&gt; Unit, onClose: () -&gt; Unit, )</ID>
<ID>LongMethod:ArticleDropdownMenu.kt$@ExperimentalMaterial3Api @Composable fun ArticleDropdownMenuIcon( modifier: Modifier, articleId: String, articleContent: String?, articleRawData: String?, authorId: String, isBookmarked: Boolean, enabled: Boolean = true, showHighlights: Boolean? = null, onToggleHighlightsClick: (() -&gt; Unit)? = null, onBookmarkClick: (() -&gt; Unit)? = null, onMuteUserClick: (() -&gt; Unit)? = null, onReportContentClick: ((reportType: ReportType) -&gt; Unit)? = null, icon: @Composable () -&gt; Unit, )</ID>
<ID>LongMethod:ArticleFeedList.kt$@ExperimentalMaterial3Api @ExperimentalFoundationApi @Composable private fun ArticleFeedLazyColumn( articleState: ArticleContract.UiState, pagingItems: LazyPagingItems&lt;FeedArticleUi&gt;, listState: LazyListState, showPaywall: Boolean, onArticleClick: (naddr: String) -&gt; Unit, onGetPremiumClick: () -&gt; Unit, articleEventPublisher: (ArticleContract.UiEvent) -&gt; Unit, modifier: Modifier = Modifier, noContentText: String = stringResource(id = R.string.article_feed_no_content), noContentVerticalArrangement: Arrangement.Vertical = Arrangement.Center, noContentPaddingValues: PaddingValues = PaddingValues(all = 0.dp), contentPadding: PaddingValues = PaddingValues(all = 0.dp), header: @Composable (LazyItemScope.() -&gt; Unit)? = null, stickyHeader: @Composable (LazyItemScope.() -&gt; Unit)? = null, )</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import net.primal.android.articles.api.model.ArticleHighlightsRequestBody
import net.primal.android.articles.db.Article
import net.primal.android.core.coroutines.CoroutineDispatcherProvider
import net.primal.android.db.PrimalDatabase
import net.primal.android.nostr.ext.asHighlightData
import net.primal.android.nostr.model.NostrEventKind
import net.primal.android.user.accounts.active.ActiveAccountStore

Expand Down Expand Up @@ -95,10 +94,7 @@ class ArticleRepository @Inject constructor(
),
)

val highlights = highlightsResponse.highlights.map { it.asHighlightData() }
if (highlights.isNotEmpty()) {
database.highlights().upsertAll(data = highlights)
}
highlightsResponse.persistToDatabaseAsTransaction(database = database)
}

suspend fun observeArticle(articleId: String, articleAuthorId: String) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ class ArticlesApiImpl @Inject constructor(

return ArticleHighlightsResponse(
highlights = queryResult.filterNostrEvents(kind = NostrEventKind.Highlight),
legendProfiles = queryResult.findPrimalEvent(kind = NostrEventKind.PrimalLegendProfiles),
primalPremiumInfo = queryResult.findPrimalEvent(kind = NostrEventKind.PrimalPremiumInfo),
primalUserNames = queryResult.findPrimalEvent(kind = NostrEventKind.PrimalUserNames),
primalUserScores = queryResult.findPrimalEvent(kind = NostrEventKind.PrimalUserScores),
cdnResources = queryResult.filterPrimalEvents(kind = NostrEventKind.PrimalCdnResource),
profileMetadatas = queryResult.filterNostrEvents(kind = NostrEventKind.Metadata),
eventStats = queryResult.filterPrimalEvents(kind = NostrEventKind.PrimalEventStats),
relayHints = queryResult.filterPrimalEvents(kind = NostrEventKind.PrimalRelayHint),
zaps = queryResult.filterNostrEvents(kind = NostrEventKind.Zap),
primalLongFormContentWordsCount = queryResult.filterPrimalEvents(
kind = NostrEventKind.PrimalLongFormWordsCount,
),
referencedEvents = queryResult.filterPrimalEvents(kind = NostrEventKind.PrimalReferencedEvent),
highlightComments = queryResult.filterNostrEvents(kind = NostrEventKind.ShortTextNote),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package net.primal.android.articles.api.mediator

import androidx.room.withTransaction
import net.primal.android.articles.api.model.ArticleHighlightsResponse
import net.primal.android.core.ext.asMapByKey
import net.primal.android.db.PrimalDatabase
import net.primal.android.highlights.utils.mapNotNullAsHighlightComments
import net.primal.android.nostr.db.eventRelayHintsUpserter
import net.primal.android.nostr.ext.asHighlightData
import net.primal.android.nostr.ext.flatMapAsEventHintsPO
import net.primal.android.nostr.ext.flatMapNotNullAsCdnResource
import net.primal.android.nostr.ext.mapAsEventZapDO
import net.primal.android.nostr.ext.mapAsProfileDataPO
import net.primal.android.nostr.ext.mapNotNullAsEventStatsPO
import net.primal.android.nostr.ext.parseAndMapPrimalLegendProfiles
import net.primal.android.nostr.ext.parseAndMapPrimalPremiumInfo
import net.primal.android.nostr.ext.parseAndMapPrimalUserNames

suspend fun ArticleHighlightsResponse.persistToDatabaseAsTransaction(database: PrimalDatabase) {
val cdnResources = this.cdnResources.flatMapNotNullAsCdnResource().asMapByKey { it.url }
val eventHints = this.relayHints.flatMapAsEventHintsPO()

val primalUserNames = this.primalUserNames.parseAndMapPrimalUserNames()
val primalPremiumInfo = this.primalPremiumInfo.parseAndMapPrimalPremiumInfo()
val primalLegendProfiles = this.legendProfiles.parseAndMapPrimalLegendProfiles()

val profiles = this.profileMetadatas.mapAsProfileDataPO(
cdnResources = cdnResources,
primalUserNames = primalUserNames,
primalPremiumInfo = primalPremiumInfo,
primalLegendProfiles = primalLegendProfiles,
)

val highlights = this.highlights.map { it.asHighlightData() }
val highlightComments = this.highlightComments.mapNotNullAsHighlightComments(highlights = highlights)

val eventZaps = this.zaps.mapAsEventZapDO(profilesMap = profiles.associateBy { it.ownerId })
val eventStats = this.eventStats.mapNotNullAsEventStatsPO()

database.withTransaction {
database.profiles().insertOrUpdateAll(data = profiles)
database.posts().upsertAll(data = highlightComments)
database.eventStats().upsertAll(data = eventStats)
database.eventZaps().upsertAll(data = eventZaps)
database.highlights().upsertAll(data = highlights)

val eventHintsDao = database.eventHints()
val hintsMap = eventHints.associateBy { it.eventId }
eventRelayHintsUpserter(dao = eventHintsDao, eventIds = eventHints.map { it.eventId }) {
copy(relays = hintsMap[this.eventId]?.relays ?: emptyList())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@ package net.primal.android.articles.api.model

import kotlinx.serialization.Serializable
import net.primal.android.nostr.model.NostrEvent
import net.primal.android.nostr.model.primal.PrimalEvent

@Serializable
data class ArticleHighlightsResponse(
val highlights: List<NostrEvent>,
val legendProfiles: PrimalEvent?,
val primalPremiumInfo: PrimalEvent?,
val primalUserNames: PrimalEvent?,
val primalUserScores: PrimalEvent?,
val cdnResources: List<PrimalEvent>,
val profileMetadatas: List<NostrEvent>,
val eventStats: List<PrimalEvent>,
val relayHints: List<PrimalEvent>,
val highlightComments: List<NostrEvent>,
val zaps: List<NostrEvent>,
val primalLongFormContentWordsCount: List<PrimalEvent>,
val referencedEvents: List<PrimalEvent>,
)
5 changes: 3 additions & 2 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.Highlight
import net.primal.android.highlights.db.HighlightData
import net.primal.android.profile.db.ProfileData
import net.primal.android.stats.db.EventStats
Expand All @@ -28,6 +29,6 @@ data class Article(
@Relation(entityColumn = "tagValue", parentColumn = "aTag")
val bookmark: PublicBookmark? = null,

@Relation(entityColumn = "referencedEventATag", parentColumn = "aTag")
val highlights: List<HighlightData> = emptyList(),
@Relation(entity = HighlightData::class, entityColumn = "referencedEventATag", parentColumn = "aTag")
val highlights: List<Highlight> = emptyList(),
)
18 changes: 18 additions & 0 deletions app/src/main/kotlin/net/primal/android/highlights/db/Highlight.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package net.primal.android.highlights.db

import androidx.room.Embedded
import androidx.room.Relation
import net.primal.android.notes.db.PostData
import net.primal.android.notes.db.PostWithAuthorData
import net.primal.android.profile.db.ProfileData

data class Highlight(
@Embedded
val data: HighlightData,

@Relation(entityColumn = "ownerId", parentColumn = "authorId")
val author: ProfileData? = null,

@Relation(entity = PostData::class, entityColumn = "replyToPostId", parentColumn = "highlightId")
val comments: List<PostWithAuthorData> = emptyList(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package net.primal.android.highlights.model

import java.time.Instant
import net.primal.android.attachments.domain.CdnImage
import net.primal.android.notes.db.PostWithAuthorData
import net.primal.android.premium.legend.LegendaryCustomization
import net.primal.android.premium.legend.asLegendaryCustomization

data class CommentUi(
val commentId: String,
val authorId: String,
val authorDisplayName: String?,
val authorInternetIdentifier: String?,
val authorLegendaryCustomization: LegendaryCustomization?,
val authorCdnImage: CdnImage?,
val content: String,
val createdAt: Instant,
)

fun PostWithAuthorData.toCommentUi() =
CommentUi(
commentId = this.post.postId,
authorId = this.author.ownerId,
authorDisplayName = this.author.displayName,
authorInternetIdentifier = this.author.internetIdentifier,
authorLegendaryCustomization = this.author.primalPremiumInfo?.legendProfile?.asLegendaryCustomization(),
authorCdnImage = this.author.avatarCdnImage,
content = this.post.content,
createdAt = Instant.ofEpochSecond(this.post.createdAt),
)
Original file line number Diff line number Diff line change
@@ -1,26 +1,58 @@
package net.primal.android.highlights.model

import net.primal.android.highlights.db.HighlightData
import net.primal.android.core.compose.profile.model.ProfileDetailsUi
import net.primal.android.core.compose.profile.model.asProfileDetailsUi
import net.primal.android.highlights.db.Highlight

data class HighlightUi(
val highlightId: String,
val authorId: String,
val author: ProfileDetailsUi?,
val content: String,
val context: String?,
val alt: String?,
val referencedEventATag: String?,
val referencedEventAuthorId: String?,
val createdAt: Long,
val comments: List<CommentUi>,
)

fun HighlightData.asHighlightUi() =
data class JoinedHighlightsUi(
val highlightId: String,
val authors: Set<ProfileDetailsUi>,
val content: String,
val comments: List<CommentUi>,
)

fun Highlight.asHighlightUi() =
HighlightUi(
highlightId = highlightId,
authorId = authorId,
content = content,
context = context,
alt = alt,
referencedEventATag = referencedEventATag,
referencedEventAuthorId = referencedEventAuthorId,
createdAt = createdAt,
highlightId = this.data.highlightId,
author = this.author?.asProfileDetailsUi(),
content = this.data.content,
context = this.data.context,
alt = this.data.alt,
referencedEventATag = this.data.referencedEventATag,
referencedEventAuthorId = this.data.referencedEventAuthorId,
createdAt = this.data.createdAt,
comments = this.comments.map { it.toCommentUi() },
)

operator fun JoinedHighlightsUi.plus(element: JoinedHighlightsUi): JoinedHighlightsUi =
JoinedHighlightsUi(
highlightId = this.highlightId,
authors = this.authors + element.authors,
content = this.content,
comments = this.comments + element.comments,
)

fun List<Highlight>.joinOnContent(): List<JoinedHighlightsUi> = this.groupBy { it.data.content }.map { it.value.sum() }

fun List<Highlight>.sum() =
this.map { it.asJoinedHighlightsUi() }.reduce { acc, joinedHighlightsUi -> acc + joinedHighlightsUi }

fun Highlight.asJoinedHighlightsUi() =
JoinedHighlightsUi(
highlightId = this.data.highlightId,
authors = setOfNotNull(this.author?.asProfileDetailsUi()),
content = this.data.content,
comments = this.comments.map { it.toCommentUi() },
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package net.primal.android.highlights.utils

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonArray
import net.primal.android.core.serialization.json.NostrJson
import net.primal.android.core.serialization.json.toJsonObject
import net.primal.android.core.utils.parseHashtags
import net.primal.android.core.utils.parseUris
import net.primal.android.highlights.db.HighlightData
import net.primal.android.nostr.ext.getTagValueOrNull
import net.primal.android.nostr.ext.hasReplyMarker
import net.primal.android.nostr.ext.hasRootMarker
import net.primal.android.nostr.ext.isEventIdTag
import net.primal.android.nostr.model.NostrEvent
import net.primal.android.notes.db.PostData

fun List<NostrEvent>.mapNotNullAsHighlightComments(highlights: List<HighlightData>): List<PostData> =
this.mapNotNull { it.asHighlightComment(highlights = highlights) }

fun NostrEvent.asHighlightComment(highlights: List<HighlightData>): PostData? {
if (!this.tags.containsRootOrReplyTag()) {
return null
}

val replyToPostId = this.tags.find { it.isEventIdTag() }?.getTagValueOrNull()

val replyToAuthorId = highlights.find { it.highlightId == replyToPostId }?.authorId

return PostData(
postId = this.id,
authorId = this.pubKey,
createdAt = this.createdAt,
tags = this.tags,
content = this.content,
uris = this.content.parseUris(),
hashtags = this.parseHashtags(),
sig = this.sig,
raw = NostrJson.encodeToString(this.toJsonObject()),
replyToPostId = replyToPostId,
replyToAuthorId = replyToAuthorId,
)
}

fun List<JsonArray>.containsRootOrReplyTag() = this.any { it.hasRootMarker() || it.hasReplyMarker() }
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ fun NostrEvent.asHighlightData() =
alt = this.tags.findFirstAltDescription(),
context = this.tags.findFirstContextTag(),
referencedEventATag = this.tags.findFirstReplaceableEventId(),
referencedEventAuthorId = this.tags.findFirstProfileId()?.extractProfileId(),
referencedEventAuthorId = this.tags.findFirstProfileId(),
createdAt = this.createdAt,
)
Loading