1. Refactors the use of dividers out of components

2. Refactors composables to load events, check hidden and check report
This commit is contained in:
Vitor Pamplona
2024-03-20 13:47:47 -04:00
parent 943a4260ff
commit 3226e4e024
18 changed files with 309 additions and 492 deletions
@@ -47,7 +47,6 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.quartz.utils.TimeUtils
@Composable
@@ -134,10 +133,5 @@ fun ChatHeaderLayout(
}
}
}
HorizontalDivider(
modifier = StdTopPadding,
thickness = DividerThickness,
)
}
}
@@ -64,7 +64,6 @@ fun BadgeCompose(
likeSetCard: BadgeCard,
isInnerNote: Boolean = false,
routeForLastRead: String,
showHidden: Boolean = false,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -79,7 +78,7 @@ fun BadgeCompose(
val scope = rememberCoroutineScope()
if (note == null) {
BlankNote(Modifier, !isInnerNote)
BlankNote(Modifier)
} else {
val backgroundColor =
calculateBackgroundColor(
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -45,7 +44,6 @@ import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import kotlinx.collections.immutable.ImmutableSet
@@ -60,7 +58,6 @@ fun BlankNotePreview() {
@Composable
fun BlankNote(
modifier: Modifier = Modifier,
showDivider: Boolean = true,
idHex: String? = null,
) {
Column(modifier = modifier) {
@@ -84,13 +81,6 @@ fun BlankNote(
textAlign = TextAlign.Center,
)
}
if (showDivider) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 10.dp),
thickness = DividerThickness,
)
}
}
}
}
@@ -171,10 +161,6 @@ fun HiddenNote(
}
}
}
HorizontalDivider(
thickness = DividerThickness,
)
}
}
@@ -189,7 +175,6 @@ fun HiddenNoteByMePreview() {
@Composable
fun HiddenNoteByMe(
modifier: Modifier = Modifier,
isQuote: Boolean = false,
onClick: () -> Unit,
) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
@@ -222,11 +207,5 @@ fun HiddenNoteByMe(
}
}
}
if (!isQuote) {
HorizontalDivider(
thickness = DividerThickness,
)
}
}
}
@@ -0,0 +1,165 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun CheckHiddenFeedWatchBlockAndReport(
note: Note,
modifier: Modifier = Modifier,
showHiddenWarning: Boolean,
showHidden: Boolean = false,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
if (showHidden) {
// Ignores reports as well
normalNote(true)
} else {
WatchBlockAndReport(note, showHiddenWarning, modifier, accountViewModel, nav) { canPreview ->
normalNote(canPreview)
}
}
}
@Composable
fun WatchBlockAndReport(
note: Note,
showHiddenWarning: Boolean,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
val isHiddenState by remember(note) {
accountViewModel.account.liveHiddenUsers
.map { note.isHiddenFor(it) }
.distinctUntilChanged()
}
.observeAsState(accountViewModel.isNoteHidden(note))
val showAnyway =
remember {
mutableStateOf(false)
}
Crossfade(targetState = isHiddenState, label = "CheckHiddenNoteCompose") { isHidden ->
if (showAnyway.value) {
normalNote(true)
} else if (!isHidden) {
LoadReportsNoteCompose(note, modifier, accountViewModel, nav) { canPreview ->
normalNote(canPreview)
}
} else if (showHiddenWarning) {
// if it is a quoted or boosted note, how the hidden warning.
HiddenNoteByMe {
showAnyway.value = true
}
}
}
}
@Composable
private fun LoadReportsNoteCompose(
note: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
var state by
remember(note) {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
Crossfade(targetState = state, label = "LoadedNoteCompose") {
RenderReportState(state = it, note = note, modifier = modifier, accountViewModel = accountViewModel, nav = nav) { canPreview ->
normalNote(canPreview)
}
}
}
@Composable
private fun RenderReportState(
state: AccountViewModel.NoteComposeReportState,
note: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
var showReportedNote by remember(note) { mutableStateOf(false) }
Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "RenderReportState") { showHiddenNote ->
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
modifier,
nav,
onClick = { showReportedNote = true },
)
} else {
val canPreview = (!state.isAcceptable && showReportedNote) || state.canPreview
normalNote(canPreview)
}
}
}
@Composable
fun WatchForReports(
note: Note,
accountViewModel: AccountViewModel,
onChange: (AccountViewModel.NoteComposeReportState) -> Unit,
) {
val userFollowsState by accountViewModel.userFollows.observeAsState()
val noteReportsState by note.live().reports.observeAsState()
val userBlocks by accountViewModel.account.flowHiddenUsers.collectAsStateWithLifecycle()
LaunchedEffect(key1 = noteReportsState, key2 = userFollowsState, userBlocks) {
accountViewModel.isNoteAcceptable(note, onChange)
}
}
@@ -21,9 +21,7 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -36,7 +34,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -49,7 +46,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Alignment.Companion.TopEnd
@@ -81,7 +77,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
@@ -106,7 +101,6 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ChannelCardCompose(
baseNote: Note,
@@ -114,160 +108,29 @@ fun ChannelCardCompose(
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
forceEventKind: Int?,
showHidden: Boolean = false,
isHiddenFeed: Boolean = false,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = hasEvent, label = "ChannelCardCompose") {
if (it) {
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) {
CheckHiddenChannelCardCompose(
baseNote,
routeForLastRead,
modifier,
parentBackgroundColor,
showHidden,
accountViewModel,
nav,
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) {
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) {
CheckHiddenFeedWatchBlockAndReport(
note = baseNote,
modifier = modifier,
showHidden = isHiddenFeed,
showHiddenWarning = false,
accountViewModel = accountViewModel,
nav = nav,
) { canPreview ->
NormalChannelCard(
baseNote = baseNote,
routeForLastRead = routeForLastRead,
modifier = modifier,
parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup,
->
BlankNote(
remember {
modifier.combinedClickable(
onClick = {},
onLongClick = showPopup,
)
},
false,
)
}
}
}
}
@Composable
fun CheckHiddenChannelCardCompose(
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
showHidden: Boolean,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
if (showHidden) {
val state by remember {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
RenderChannelCardReportState(
state = state,
note = note,
routeForLastRead = routeForLastRead,
modifier = modifier,
parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
} else {
val isHidden by
accountViewModel.account.liveHiddenUsers
.map { note.isHiddenFor(it) }
.distinctUntilChanged()
.observeAsState(accountViewModel.isNoteHidden(note))
Crossfade(targetState = isHidden, label = "CheckHiddenChannelCardCompose") {
if (!it) {
LoadedChannelCardCompose(
note,
routeForLastRead,
modifier,
parentBackgroundColor,
accountViewModel,
nav,
)
}
}
}
}
@Composable
fun LoadedChannelCardCompose(
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
var state by remember {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
val scope = rememberCoroutineScope()
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
scope.launch(Dispatchers.Main) { state = newState }
}
}
Crossfade(targetState = state, label = "CheckHiddenChannelCardCompose") {
RenderChannelCardReportState(
it,
note,
routeForLastRead,
modifier,
parentBackgroundColor,
accountViewModel,
nav,
)
}
}
@Composable
fun RenderChannelCardReportState(
state: AccountViewModel.NoteComposeReportState,
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
var showReportedNote by remember { mutableStateOf(false) }
Crossfade(
targetState = !state.isAcceptable && !showReportedNote,
label = "CheckHiddenChannelCardCompose",
) { showHiddenNote ->
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
modifier,
nav,
onClick = { showReportedNote = true },
)
} else {
NormalChannelCard(
note,
routeForLastRead,
modifier,
parentBackgroundColor,
accountViewModel,
nav,
)
}
}
}
@@ -368,10 +231,6 @@ fun InnerCardRow(
)
}
}
HorizontalDivider(
thickness = DividerThickness,
)
}
@Composable
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
@@ -32,7 +31,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -61,9 +59,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -97,7 +94,6 @@ import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ChatroomMessageCompose(
baseNote: Note,
@@ -108,111 +104,13 @@ fun ChatroomMessageCompose(
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit,
) {
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = hasEvent) {
if (it) {
CheckHiddenChatMessage(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply,
)
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup,
->
BlankNote(
remember {
Modifier.combinedClickable(
onClick = {},
onLongClick = showPopup,
)
},
)
}
}
}
}
@Composable
fun CheckHiddenChatMessage(
baseNote: Note,
routeForLastRead: String?,
innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit,
) {
val isHidden by
remember {
accountViewModel.account.liveHiddenUsers
.map { baseNote.isHiddenFor(it) }
.distinctUntilChanged()
}
.observeAsState(accountViewModel.isNoteHidden(baseNote))
if (!isHidden) {
LoadedChatMessageCompose(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply,
)
}
}
@Composable
fun LoadedChatMessageCompose(
baseNote: Note,
routeForLastRead: String?,
innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit,
) {
var state by remember {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
WatchForReports(baseNote, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
var showReportedNote by remember { mutableStateOf(false) }
val showHiddenNote by
remember(state, showReportedNote) {
derivedStateOf { !state.isAcceptable && !showReportedNote }
}
Crossfade(targetState = showHiddenNote) {
if (it) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
Modifier,
nav,
onClick = { showReportedNote = true },
)
} else {
val canPreview by
remember(state, showReportedNote) {
derivedStateOf { (!state.isAcceptable && showReportedNote) || state.canPreview }
}
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) {
WatchBlockAndReport(
note = baseNote,
showHiddenWarning = innerQuote,
accountViewModel = accountViewModel,
nav = nav,
) { canPreview ->
NormalChatNote(
baseNote,
routeForLastRead,
@@ -104,7 +104,7 @@ fun MessageSetCompose(
baseNote = baseNote,
routeForLastRead = null,
isBoostedNote = true,
showHidden = showHidden,
isHiddenFeed = showHidden,
quotesLeft = 1,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
@@ -145,7 +145,7 @@ fun MultiSetCompose(
routeForLastRead = null,
modifier = HalfTopPadding,
isBoostedNote = true,
showHidden = showHidden,
isHiddenFeed = showHidden,
quotesLeft = 1,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
@@ -42,7 +41,6 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
@@ -51,7 +49,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
@@ -176,7 +173,7 @@ fun NoteCompose(
isQuotedNote: Boolean = false,
unPackReply: Boolean = true,
makeItShort: Boolean = false,
showHidden: Boolean = false,
isHiddenFeed: Boolean = false,
quotesLeft: Int,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
@@ -185,13 +182,12 @@ fun NoteCompose(
WatchNoteEvent(
baseNote = baseNote,
accountViewModel = accountViewModel,
showDivider = !isBoostedNote && !isQuotedNote,
modifier,
) {
CheckHiddenNoteCompose(
CheckHiddenFeedWatchBlockAndReport(
note = baseNote,
modifier = modifier,
showHidden = showHidden,
showHidden = isHiddenFeed,
showHiddenWarning = isQuotedNote || isBoostedNote,
accountViewModel = accountViewModel,
nav = nav,
@@ -214,171 +210,6 @@ fun NoteCompose(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun WatchNoteEvent(
baseNote: Note,
accountViewModel: AccountViewModel,
showDivider: Boolean,
modifier: Modifier = Modifier,
onNoteEventFound: @Composable () -> Unit,
) {
if (baseNote.event != null) {
onNoteEventFound()
} else {
// avoid observing costs if already has an event.
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = hasEvent, label = "Event presence") {
if (it) {
onNoteEventFound()
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = {},
onLongClick = showPopup,
)
},
showDivider,
)
}
}
}
}
}
@Composable
fun CheckHiddenNoteCompose(
note: Note,
modifier: Modifier = Modifier,
showHiddenWarning: Boolean,
showHidden: Boolean = false,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
if (showHidden) {
// Ignores reports as well
normalNote(true)
} else {
WatchIsHidden(note, showHiddenWarning, modifier, accountViewModel, nav) { canPreview ->
normalNote(canPreview)
}
}
}
@Composable
fun WatchIsHidden(
note: Note,
showHiddenWarning: Boolean,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
val isHiddenState by remember(note) {
accountViewModel.account.liveHiddenUsers
.map { note.isHiddenFor(it) }
.distinctUntilChanged()
}
.observeAsState(accountViewModel.isNoteHidden(note))
val showAnyway =
remember {
mutableStateOf(false)
}
Crossfade(targetState = isHiddenState, label = "CheckHiddenNoteCompose") { isHidden ->
if (showAnyway.value) {
normalNote(true)
} else if (!isHidden) {
LoadReportsNoteCompose(note, modifier, accountViewModel, nav) { canPreview ->
normalNote(canPreview)
}
} else if (showHiddenWarning) {
// if it is a quoted or boosted note, how the hidden warning.
HiddenNoteByMe(
isQuote = true,
onClick = { showAnyway.value = true },
)
}
}
}
@Composable
fun LoadReportsNoteCompose(
note: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
var state by
remember(note) {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
Crossfade(targetState = state, label = "LoadedNoteCompose") {
RenderReportState(state = it, note = note, modifier = modifier, accountViewModel = accountViewModel, nav = nav) { canPreview ->
normalNote(canPreview)
}
}
}
@Composable
fun RenderReportState(
state: AccountViewModel.NoteComposeReportState,
note: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
var showReportedNote by remember(note) { mutableStateOf(false) }
Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "RenderReportState") { showHiddenNote ->
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
modifier,
nav,
onClick = { showReportedNote = true },
)
} else {
val canPreview = (!state.isAcceptable && showReportedNote) || state.canPreview
normalNote(canPreview)
}
}
}
@Composable
fun WatchForReports(
note: Note,
accountViewModel: AccountViewModel,
onChange: (AccountViewModel.NoteComposeReportState) -> Unit,
) {
val userFollowsState by accountViewModel.userFollows.observeAsState()
val noteReportsState by note.live().reports.observeAsState()
val userBlocks by accountViewModel.account.flowHiddenUsers.collectAsStateWithLifecycle()
LaunchedEffect(key1 = noteReportsState, key2 = userFollowsState, userBlocks) {
accountViewModel.isNoteAcceptable(note, onChange)
}
}
@Composable
fun AcceptableNote(
baseNote: Note,
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun WatchNoteEvent(
baseNote: Note,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
onNoteEventFound: @Composable () -> Unit,
) {
if (baseNote.event != null) {
onNoteEventFound()
} else {
// avoid observing costs if already has an event.
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = hasEvent, label = "Event presence") {
if (it) {
onNoteEventFound()
} else {
LongPressToQuickAction(
baseNote = baseNote,
accountViewModel = accountViewModel,
) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = {},
onLongClick = showPopup,
)
},
)
}
}
}
}
}
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -54,7 +53,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.WatchIsHiddenUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.LnZapEvent
@@ -77,11 +75,11 @@ fun ZapNoteCompose(
}
}
val route = remember(baseAuthor) { "User/${baseAuthor?.pubkeyHex}" }
if (baseAuthor == null) {
BlankNote()
} else {
val route = remember(baseAuthor) { "User/${baseAuthor?.pubkeyHex}" }
Column(
modifier =
Modifier.clickable(
@@ -90,11 +88,6 @@ fun ZapNoteCompose(
verticalArrangement = Arrangement.Center,
) {
baseAuthor?.let { RenderZapNote(it, baseReqResponse.zapEvent, nav, accountViewModel) }
HorizontalDivider(
modifier = Modifier.padding(top = 10.dp),
thickness = DividerThickness,
)
}
}
}
@@ -265,7 +265,6 @@ private fun RenderCardItem(
BadgeCompose(
item,
accountViewModel = accountViewModel,
showHidden = showHidden,
nav = nav,
routeForLastRead = routeForLastRead,
)
@@ -304,7 +303,7 @@ fun NoteCardCompose(
isQuotedNote = isQuotedNote,
unPackReply = unPackReply,
makeItShort = makeItShort,
showHidden = showHidden,
isHiddenFeed = showHidden,
quotesLeft = 3,
parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel,
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
@@ -35,7 +36,9 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.note.ChatroomHeaderCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import kotlin.time.ExperimentalTime
@Composable
@@ -109,6 +112,11 @@ private fun FeedLoaded(
nav = nav,
)
}
HorizontalDivider(
modifier = StdTopPadding,
thickness = DividerThickness,
)
}
}
}
@@ -228,7 +228,7 @@ private fun FeedLoaded(
routeForLastRead = routeForLastRead,
modifier = Modifier,
isBoostedNote = false,
showHidden = state.showHidden.value,
isHiddenFeed = state.showHidden.value,
quotesLeft = 3,
accountViewModel = accountViewModel,
nav = nav,
@@ -22,14 +22,19 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@Composable
@@ -72,6 +77,11 @@ private fun LnZapFeedLoaded(
) {
itemsIndexed(state.feed.value, key = { _, item -> item.zapEvent.idHex }) { _, item ->
ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav)
HorizontalDivider(
modifier = Modifier.padding(top = 10.dp),
thickness = DividerThickness,
)
}
}
}
@@ -147,6 +147,7 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -288,11 +289,12 @@ fun ThreadFeedView(
accountViewModel = accountViewModel,
nav = nav,
)
HorizontalDivider(
thickness = DividerThickness,
)
}
HorizontalDivider(
modifier = StdTopPadding,
thickness = DividerThickness,
)
}
}
}
@@ -626,10 +628,6 @@ fun NoteMaster(
}
ReactionsRow(note, true, editState, accountViewModel, nav)
HorizontalDivider(
thickness = DividerThickness,
)
}
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
@@ -37,6 +37,7 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
@@ -74,6 +75,7 @@ import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.quartz.events.ChannelCreateEvent
@@ -352,6 +354,10 @@ private fun DiscoverFeedLoaded(
nav = nav,
)
}
HorizontalDivider(
thickness = DividerThickness,
)
}
}
}
@@ -384,6 +390,10 @@ private fun DiscoverFeedColumnsLoaded(
nav = nav,
)
}
HorizontalDivider(
thickness = DividerThickness,
)
}
}
}
@@ -86,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.findHashtags
import kotlinx.coroutines.Dispatchers
@@ -401,6 +402,11 @@ private fun DisplaySearchResults(
loadProfilePicture = automaticallyShowProfilePicture,
onClick = { nav("Channel/${item.idHex}") },
)
HorizontalDivider(
modifier = StdTopPadding,
thickness = DividerThickness,
)
}
itemsIndexed(