- 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
This commit is contained in:
Vitor Pamplona
2023-05-30 12:03:33 -04:00
parent 7ad982ec71
commit 5ab94e8a1c
5 changed files with 222 additions and 81 deletions
@@ -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) refreshObservers(note)
} }
@@ -541,9 +532,15 @@ object LocalCache {
mentions.forEach { mentions.forEach {
it.addReport(note) it.addReport(note)
} }
} } else {
repliesTo.forEach { repliesTo.forEach {
it.addReport(note) it.addReport(note)
}
mentions.forEach {
// doesn't add to reports, but triggers recounts
it.liveSet?.reports?.invalidateData()
}
} }
refreshObservers(note) refreshObservers(note)
@@ -15,6 +15,7 @@ import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource 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.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.isValidURL
@@ -73,7 +74,12 @@ open class NewPostViewModel : ViewModel() {
open fun load(account: Account, replyingTo: Note?, quote: Note?) { open fun load(account: Account, replyingTo: Note?, quote: Note?) {
originalNote = replyingTo originalNote = replyingTo
replyingTo?.let { replyNote -> 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 -> replyNote.author?.let { replyUser ->
val currentMentions = (replyNote.event as? TextNoteEvent) val currentMentions = (replyNote.event as? TextNoteEvent)
?.mentions() ?.mentions()
@@ -1,7 +1,9 @@
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ReportEvent
object UserProfileReportsFeedFilter : FeedFilter<Note>() { object UserProfileReportsFeedFilter : FeedFilter<Note>() {
var user: User? = null var user: User? = null
@@ -11,10 +13,12 @@ object UserProfileReportsFeedFilter : FeedFilter<Note>() {
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
return user?.reports val myUser = user ?: return emptyList()
?.values
?.flatten() val reportNotes = LocalCache.notes.values.filter { (it.event as? ReportEvent)?.isTaggedUser(myUser.pubkeyHex) == true }
?.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
?.reversed() ?: emptyList() return reportNotes
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
} }
} }
@@ -43,8 +43,10 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.lightColors import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf 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.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent 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.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.ClickableUrl
@@ -152,18 +153,10 @@ fun NoteCompose(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val noteState by baseNote.live().metadata.observeAsState() 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() if (noteEvent == null) {
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
val isSensitive = remember(noteState) { note.event?.isSensitive() ?: false }
if (note.event == null) {
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
BlankNote( BlankNote(
@@ -176,55 +169,150 @@ fun NoteCompose(
isBoostedNote isBoostedNote
) )
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) NoteQuickActionMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
} else if (account.isHidden(noteForReports.author!!) || (isSensitive && account.showSensitiveContent == false)) {
// Does nothing
} else { } else {
var showReportedNote by remember { mutableStateOf(false) } CheckHiddenNoteCompose(
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) } baseNote,
routeForLastRead,
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
parentBackgroundColor,
accountViewModel,
nav
)
}
}
LaunchedEffect(key1 = noteReportsState, key2 = accountState) { @Composable
withContext(Dispatchers.IO) { fun CheckHiddenNoteCompose(
account.userProfile().let { loggedIn -> note: Note,
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex || routeForLastRead: String? = null,
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || modifier: Modifier = Modifier,
!(noteForReports.hasAnyReports()) 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) { account.isHidden(note.author!!) || (isSensitive && account.showSensitiveContent == false)
isAcceptableAndCanPreview = Pair(newIsAcceptable, newCanPreview) }
} }
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<Note>
)
@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) { val showHiddenNote by remember(state, showReportedNote) {
HiddenNote( derivedStateOf {
account.getRelevantReports(noteForReports), !state.isAcceptable && !showReportedNote
account.userProfile(),
modifier,
isBoostedNote,
nav,
onClick = { showReportedNote = true }
)
} else {
NormalNote(
baseNote,
routeForLastRead,
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
isAcceptableAndCanPreview.second,
parentBackgroundColor,
accountViewModel,
nav
)
} }
} }
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) @OptIn(ExperimentalFoundationApi::class)
@@ -373,7 +461,7 @@ fun NormalNote(
} }
is ReportEvent -> { is ReportEvent -> {
RenderReport(baseNote) RenderReport(baseNote, backgroundColor, accountViewModel, nav)
} }
is LongTextNoteEvent -> { is LongTextNoteEvent -> {
@@ -1045,14 +1133,16 @@ private fun RenderLongFormContent(
} }
@Composable @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 base = remember {
val noteEvent = note.event as? ReportEvent (noteEvent.reportedPost() + noteEvent.reportedAuthor())
if (noteEvent == null) {
emptyList<ReportedKey>()
} else {
(noteEvent.reportedPost() + noteEvent.reportedAuthor())
}
} }
val reportType = base.map { val reportType = base.map {
@@ -1066,12 +1156,45 @@ private fun RenderReport(note: Note) {
} }
}.toSet().joinToString(", ") }.toSet().joinToString(", ")
Text( val content = remember {
text = reportType 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( Divider(
modifier = Modifier.padding(top = 40.dp), modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp thickness = 0.25.dp
) )
} }
@@ -232,7 +232,9 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String)
tabs.forEachIndexed { index, function -> tabs.forEachIndexed { index, function ->
Tab( Tab(
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } }, onClick = {
coroutineScope.launch { pagerState.animateScrollToPage(index) }
},
text = function text = function
) )
} }
@@ -275,7 +277,16 @@ private fun RelaysTabHeader(baseUser: User) {
@Composable @Composable
private fun ReportsTabHeader(baseUser: User) { private fun ReportsTabHeader(baseUser: User) {
val userState by baseUser.live().reports.observeAsState() 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)}") Text(text = "$userReports ${stringResource(R.string.reports)}")
} }