From 5ab94e8a1c7447a546ed4ef849385d5eb1617096 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 May 2023 12:03:33 -0400 Subject: [PATCH] - Renders the report's comment in the user profile report tab - Allows reply, zap, boost and like the report itself. - Shows report messages on notifications --- .../amethyst/model/LocalCache.kt | 21 +- .../amethyst/ui/actions/NewPostViewModel.kt | 8 +- .../ui/dal/UserProfileReportsFeedFilter.kt | 14 +- .../amethyst/ui/note/NoteCompose.kt | 245 +++++++++++++----- .../ui/screen/loggedIn/ProfileScreen.kt | 15 +- 5 files changed, 222 insertions(+), 81 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index eea2d7851..1d1842bc7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -505,15 +505,6 @@ object LocalCache { } } - if (event.content == "!" || // nostr_console hide. - event.content == "\u26A0\uFE0F" // Warning sign - ) { - // Counts the replies - repliesTo.forEach { - it.addReport(note) - } - } - refreshObservers(note) } @@ -541,9 +532,15 @@ object LocalCache { mentions.forEach { it.addReport(note) } - } - repliesTo.forEach { - it.addReport(note) + } else { + repliesTo.forEach { + it.addReport(note) + } + + mentions.forEach { + // doesn't add to reports, but triggers recounts + it.liveSet?.reports?.invalidateData() + } } refreshObservers(note) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 327843b60..17d792a61 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource +import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.ui.components.isValidURL @@ -73,7 +74,12 @@ open class NewPostViewModel : ViewModel() { open fun load(account: Account, replyingTo: Note?, quote: Note?) { originalNote = replyingTo replyingTo?.let { replyNote -> - this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote) + if (replyNote.event is BaseTextNoteEvent) { + this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote) + } else { + this.replyTos = listOf(replyNote) + } + replyNote.author?.let { replyUser -> val currentMentions = (replyNote.event as? TextNoteEvent) ?.mentions() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt index 12badf519..981b1b505 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt @@ -1,7 +1,9 @@ package com.vitorpamplona.amethyst.ui.dal +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.model.ReportEvent object UserProfileReportsFeedFilter : FeedFilter() { var user: User? = null @@ -11,10 +13,12 @@ object UserProfileReportsFeedFilter : FeedFilter() { } override fun feed(): List { - return user?.reports - ?.values - ?.flatten() - ?.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) - ?.reversed() ?: emptyList() + val myUser = user ?: return emptyList() + + val reportNotes = LocalCache.notes.values.filter { (it.event as? ReportEvent)?.isTaggedUser(myUser.pubkeyHex) == true } + + return reportNotes + .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) + .reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 2266b74bf..166dd8f28 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -43,8 +43,10 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.lightColors import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -102,7 +104,6 @@ import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent -import com.vitorpamplona.amethyst.service.model.ReportedKey import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.ui.components.ClickableUrl @@ -152,18 +153,10 @@ fun NoteCompose( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = remember(accountState) { accountState?.account } ?: return - val noteState by baseNote.live().metadata.observeAsState() - val note = remember(noteState) { noteState?.note } ?: return + val noteEvent = remember(noteState) { noteState?.note?.event } - val noteReportsState by baseNote.live().reports.observeAsState() - val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return - - val isSensitive = remember(noteState) { note.event?.isSensitive() ?: false } - - if (note.event == null) { + if (noteEvent == null) { var popupExpanded by remember { mutableStateOf(false) } BlankNote( @@ -176,55 +169,150 @@ fun NoteCompose( isBoostedNote ) - NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) - } else if (account.isHidden(noteForReports.author!!) || (isSensitive && account.showSensitiveContent == false)) { - // Does nothing + NoteQuickActionMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel) } else { - var showReportedNote by remember { mutableStateOf(false) } - var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) } + CheckHiddenNoteCompose( + baseNote, + routeForLastRead, + modifier, + isBoostedNote, + isQuotedNote, + unPackReply, + makeItShort, + addMarginTop, + parentBackgroundColor, + accountViewModel, + nav + ) + } +} - LaunchedEffect(key1 = noteReportsState, key2 = accountState) { - withContext(Dispatchers.IO) { - account.userProfile().let { loggedIn -> - val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex || - (note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || - !(noteForReports.hasAnyReports()) +@Composable +fun CheckHiddenNoteCompose( + note: Note, + routeForLastRead: String? = null, + modifier: Modifier = Modifier, + isBoostedNote: Boolean = false, + isQuotedNote: Boolean = false, + unPackReply: Boolean = true, + makeItShort: Boolean = false, + addMarginTop: Boolean = true, + parentBackgroundColor: Color? = null, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = remember(accountState) { accountState?.account } ?: return - val newIsAcceptable = account.isAcceptable(noteForReports) + val isHidden by remember(accountState) { + derivedStateOf { + val isSensitive = note.event?.isSensitive() ?: false - if (newIsAcceptable != isAcceptableAndCanPreview.first && newCanPreview != isAcceptableAndCanPreview.second) { - isAcceptableAndCanPreview = Pair(newIsAcceptable, newCanPreview) - } + account.isHidden(note.author!!) || (isSensitive && account.showSensitiveContent == false) + } + } + + if (!isHidden) { + LoadedNoteCompose( + note, + routeForLastRead, + modifier, + isBoostedNote, + isQuotedNote, + unPackReply, + makeItShort, + addMarginTop, + parentBackgroundColor, + accountViewModel, + nav + ) + } +} + +@Immutable +data class NoteComposeReportState( + val isAcceptable: Boolean, + val canPreview: Boolean, + val relevantReports: Set +) + +@Composable +fun LoadedNoteCompose( + note: Note, + routeForLastRead: String? = null, + modifier: Modifier = Modifier, + isBoostedNote: Boolean = false, + isQuotedNote: Boolean = false, + unPackReply: Boolean = true, + makeItShort: Boolean = false, + addMarginTop: Boolean = true, + parentBackgroundColor: Color? = null, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = remember(accountState) { accountState?.account } ?: return + + val noteReportsState by note.live().reports.observeAsState() + val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return + + var showReportedNote by remember { mutableStateOf(false) } + var state by remember { mutableStateOf(NoteComposeReportState(true, true, emptySet())) } + + LaunchedEffect(key1 = noteReportsState, key2 = accountState) { + withContext(Dispatchers.IO) { + account.userProfile().let { loggedIn -> + val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex || + (note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || + !(noteForReports.hasAnyReports()) + + val newIsAcceptable = account.isAcceptable(noteForReports) + val newRelevantReports = account.getRelevantReports(noteForReports) + + if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) { + state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports) } } } + } - if (!isAcceptableAndCanPreview.first && !showReportedNote) { - HiddenNote( - account.getRelevantReports(noteForReports), - account.userProfile(), - modifier, - isBoostedNote, - nav, - onClick = { showReportedNote = true } - ) - } else { - NormalNote( - baseNote, - routeForLastRead, - modifier, - isBoostedNote, - isQuotedNote, - unPackReply, - makeItShort, - addMarginTop, - isAcceptableAndCanPreview.second, - parentBackgroundColor, - accountViewModel, - nav - ) + val showHiddenNote by remember(state, showReportedNote) { + derivedStateOf { + !state.isAcceptable && !showReportedNote } } + + if (showHiddenNote) { + HiddenNote( + state.relevantReports, + accountViewModel.userProfile(), + modifier, + isBoostedNote, + nav, + onClick = { showReportedNote = true } + ) + } else { + val canPreview by remember(state, showReportedNote) { + derivedStateOf { + (!state.isAcceptable && showReportedNote) || state.canPreview + } + } + + NormalNote( + note, + routeForLastRead, + modifier, + isBoostedNote, + isQuotedNote, + unPackReply, + makeItShort, + addMarginTop, + canPreview, + parentBackgroundColor, + accountViewModel, + nav + ) + } } @OptIn(ExperimentalFoundationApi::class) @@ -373,7 +461,7 @@ fun NormalNote( } is ReportEvent -> { - RenderReport(baseNote) + RenderReport(baseNote, backgroundColor, accountViewModel, nav) } is LongTextNoteEvent -> { @@ -1045,14 +1133,16 @@ private fun RenderLongFormContent( } @Composable -private fun RenderReport(note: Note) { +private fun RenderReport( + note: Note, + backgroundColor: Color, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + val noteEvent = note.event as? ReportEvent ?: return + val base = remember { - val noteEvent = note.event as? ReportEvent - if (noteEvent == null) { - emptyList() - } else { - (noteEvent.reportedPost() + noteEvent.reportedAuthor()) - } + (noteEvent.reportedPost() + noteEvent.reportedAuthor()) } val reportType = base.map { @@ -1066,12 +1156,45 @@ private fun RenderReport(note: Note) { } }.toSet().joinToString(", ") - Text( - text = reportType + val content = remember { + reportType + (note.event?.content()?.ifBlank { null }?.let { ": $it" } ?: "") + } + + TranslatableRichTextViewer( + content = content, + canPreview = true, + modifier = remember { Modifier }, + tags = emptyList(), + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav ) + note.replyTo?.lastOrNull()?.let { + NoteCompose( + baseNote = it, + isQuotedNote = true, + modifier = Modifier + .padding(top = 5.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ), + unPackReply = false, + makeItShort = true, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav + ) + } + + ReactionsRow(note, accountViewModel, nav) + Divider( - modifier = Modifier.padding(top = 40.dp), + modifier = Modifier.padding(top = 10.dp), thickness = 0.25.dp ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index deeeccef8..6300c07db 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -232,7 +232,9 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String) tabs.forEachIndexed { index, function -> Tab( selected = pagerState.currentPage == index, - onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } }, + onClick = { + coroutineScope.launch { pagerState.animateScrollToPage(index) } + }, text = function ) } @@ -275,7 +277,16 @@ private fun RelaysTabHeader(baseUser: User) { @Composable private fun ReportsTabHeader(baseUser: User) { val userState by baseUser.live().reports.observeAsState() - val userReports = remember(userState) { userState?.user?.reports?.values?.flatten()?.count() } + var userReports by remember { mutableStateOf(0) } + + LaunchedEffect(key1 = userState) { + UserProfileReportsFeedFilter.user = baseUser + val newSize = UserProfileReportsFeedFilter.feed().size + + if (newSize != userReports) { + userReports = newSize + } + } Text(text = "$userReports ${stringResource(R.string.reports)}") }