- Merges Hidden and Reporting flows

- Removes Report Live data
- Refactors Full Bleed design
- Unifies Hidden and Report checks between the Video Feed, the Full Bleed Design and the Card layout.
This commit is contained in:
Vitor Pamplona 2024-06-12 16:54:39 -04:00
parent 0d00f2f80a
commit 45445c03e5
7 changed files with 432 additions and 574 deletions

View File

@ -258,7 +258,9 @@ open class Note(
if (repliesChanged) liveSet?.innerReplies?.invalidateData()
if (reactionsChanged) liveSet?.innerReactions?.invalidateData()
if (boostsChanged) liveSet?.innerBoosts?.invalidateData()
if (reportsChanged) liveSet?.innerReports?.invalidateData()
if (reportsChanged) {
flowSet?.reports?.invalidateData()
}
if (zapsChanged) liveSet?.innerZaps?.invalidateData()
return toBeRemoved
@ -290,7 +292,7 @@ open class Note(
if (reports[author]?.contains(deleteNote) == true) {
reports[author]?.let {
reports = reports + Pair(author, it.minus(deleteNote))
liveSet?.innerReports?.invalidateData()
flowSet?.reports?.invalidateData()
}
}
}
@ -399,10 +401,10 @@ open class Note(
if (reportsByAuthor == null) {
reports = reports + Pair(author, listOf(note))
liveSet?.innerReports?.invalidateData()
flowSet?.reports?.invalidateData()
} else if (!reportsByAuthor.contains(note)) {
reports = reports + Pair(author, reportsByAuthor + note)
liveSet?.innerReports?.invalidateData()
flowSet?.reports?.invalidateData()
}
}
@ -844,11 +846,13 @@ class NoteFlowSet(
) {
// Observers line up here.
val metadata = NoteBundledRefresherFlow(u)
val reports = NoteBundledRefresherFlow(u)
fun isInUse(): Boolean = metadata.stateFlow.subscriptionCount.value > 0
fun isInUse(): Boolean = metadata.stateFlow.subscriptionCount.value > 0 || reports.stateFlow.subscriptionCount.value > 0
fun destroy() {
metadata.destroy()
reports.destroy()
}
}
@ -861,7 +865,6 @@ class NoteLiveSet(
val innerReactions = NoteBundledRefresherLiveData(u)
val innerBoosts = NoteBundledRefresherLiveData(u)
val innerReplies = NoteBundledRefresherLiveData(u)
val innerReports = NoteBundledRefresherLiveData(u)
val innerRelays = NoteBundledRefresherLiveData(u)
val innerZaps = NoteBundledRefresherLiveData(u)
val innerOts = NoteBundledRefresherLiveData(u)
@ -871,7 +874,6 @@ class NoteLiveSet(
val reactions = innerReactions.map { it }
val boosts = innerBoosts.map { it }
val replies = innerReplies.map { it }
val reports = innerReports.map { it }
val relays = innerRelays.map { it }
val zaps = innerZaps.map { it }
@ -907,7 +909,6 @@ class NoteLiveSet(
reactions.hasObservers() ||
boosts.hasObservers() ||
replies.hasObservers() ||
reports.hasObservers() ||
relays.hasObservers() ||
zaps.hasObservers() ||
hasEvent.hasObservers() ||
@ -923,7 +924,6 @@ class NoteLiveSet(
innerReactions.destroy()
innerBoosts.destroy()
innerReplies.destroy()
innerReports.destroy()
innerRelays.destroy()
innerZaps.destroy()
innerOts.destroy()

View File

@ -22,9 +22,7 @@ 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
@ -38,12 +36,12 @@ fun CheckHiddenFeedWatchBlockAndReport(
note: Note,
modifier: Modifier = Modifier,
showHiddenWarning: Boolean,
showHidden: Boolean = false,
ignoreAllBlocksAndReports: Boolean = false,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
if (showHidden) {
if (ignoreAllBlocksAndReports) {
// Ignores reports as well
normalNote(true)
} else {
@ -62,19 +60,28 @@ fun WatchBlockAndReport(
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
val isHiddenState by accountViewModel.createIsHiddenFlow(note).collectAsStateWithLifecycle()
val hiddenState by accountViewModel.createIsHiddenFlow(note).collectAsStateWithLifecycle()
val showAnyway =
remember {
mutableStateOf(false)
}
Crossfade(targetState = isHiddenState, label = "CheckHiddenNoteCompose") { isHidden ->
Crossfade(targetState = hiddenState, label = "CheckHiddenNoteCompose") { isHidden ->
if (showAnyway.value) {
normalNote(true)
} else if (!isHidden) {
LoadReportsNoteCompose(note, modifier, accountViewModel, nav) { canPreview ->
normalNote(canPreview)
} else if (!isHidden.isPostHidden) {
if (isHidden.isAcceptable) {
normalNote(isHidden.canPreview)
} else {
HiddenNote(
isHidden.relevantReports,
isHidden.isHiddenAuthor,
accountViewModel,
modifier,
nav,
onClick = { showAnyway.value = true },
)
}
} else if (showHiddenWarning) {
// if it is a quoted or boosted note, how the hidden warning.
@ -84,75 +91,3 @@ fun WatchBlockAndReport(
}
}
}
@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)
}
}

View File

@ -49,7 +49,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier
@ -124,7 +123,7 @@ fun ChannelCardCompose(
CheckHiddenFeedWatchBlockAndReport(
note = baseNote,
modifier = modifier,
showHidden = isHiddenFeed,
ignoreAllBlocksAndReports = isHiddenFeed,
showHiddenWarning = false,
accountViewModel = accountViewModel,
nav = nav,
@ -308,8 +307,7 @@ fun RenderClassifiedsThumb(
title = noteEvent?.title(),
price = noteEvent?.price(),
)
}
.distinctUntilChanged()
}.distinctUntilChanged()
.observeAsState(
ClassifiedsThumb(
image = noteEvent.image(),
@ -437,8 +435,7 @@ fun RenderLiveActivityThumb(
status = noteEvent?.status(),
starts = noteEvent?.starts(),
)
}
.distinctUntilChanged()
}.distinctUntilChanged()
.observeAsState(
LiveActivityCard(
name = noteEvent.dTag(),
@ -567,8 +564,7 @@ fun RenderCommunitiesThumb(
cover = noteEvent?.image()?.ifBlank { null },
moderators = noteEvent?.moderators()?.toImmutableList() ?: persistentListOf(),
)
}
.distinctUntilChanged()
}.distinctUntilChanged()
.observeAsState(
CommunityCard(
name = noteEvent.dTag(),
@ -672,7 +668,9 @@ fun LoadModerators(
}
}
val followingKeySet = accountViewModel.account.liveDiscoveryFollowLists.value?.users
val followingKeySet =
accountViewModel.account.liveDiscoveryFollowLists.value
?.users
val allParticipants =
ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
@ -723,7 +721,9 @@ private fun LoadParticipants(
val hostsAuthor = hosts + (baseNote.author?.let { listOf(it) } ?: emptyList<User>())
val followingKeySet = accountViewModel.account.liveDiscoveryFollowLists.value?.users
val followingKeySet =
accountViewModel.account.liveDiscoveryFollowLists.value
?.users
val allParticipants =
ParticipantListBuilder()
@ -760,7 +760,11 @@ fun RenderContentDVMThumb(
nav: (String) -> Unit,
) {
// downloads user metadata to pre-load the NIP-65 relays.
val user = baseNote.author?.live()?.metadata?.observeAsState()
val user =
baseNote.author
?.live()
?.metadata
?.observeAsState()
val card = observeAppDefinition(appDefinitionNote = baseNote)
@ -875,7 +879,9 @@ fun RenderChannelThumb(
LaunchedEffect(key1 = channelUpdates) {
launch(Dispatchers.IO) {
val followingKeySet = accountViewModel.account.liveDiscoveryFollowLists.value?.users
val followingKeySet =
accountViewModel.account.liveDiscoveryFollowLists.value
?.users
val allParticipants =
ParticipantListBuilder()
.followsThatParticipateOn(baseNote, followingKeySet)

View File

@ -204,7 +204,7 @@ fun NoteCompose(
CheckHiddenFeedWatchBlockAndReport(
note = baseNote,
modifier = modifier,
showHidden = isHiddenFeed,
ignoreAllBlocksAndReports = isHiddenFeed,
showHiddenWarning = isQuotedNote || isBoostedNote,
accountViewModel = accountViewModel,
nav = nav,
@ -264,9 +264,7 @@ fun AcceptableNote(
}
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) {
showPopup,
->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote(
baseNote = baseNote,
routeForLastRead = routeForLastRead,

View File

@ -36,11 +36,8 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
@ -49,9 +46,9 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
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
@ -62,7 +59,6 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
@ -85,19 +81,21 @@ import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
import com.vitorpamplona.amethyst.ui.note.DisplayDraft
import com.vitorpamplona.amethyst.ui.note.DisplayOtsIfInOriginal
import com.vitorpamplona.amethyst.ui.note.HiddenNote
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
import com.vitorpamplona.amethyst.ui.note.RenderDraft
import com.vitorpamplona.amethyst.ui.note.RenderRepost
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus
import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingCommunityInPost
@ -107,15 +105,14 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW
import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward
import com.vitorpamplona.amethyst.ui.note.elements.DisplayZapSplits
import com.vitorpamplona.amethyst.ui.note.elements.ForkInformationRow
import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.note.elements.Reward
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
import com.vitorpamplona.amethyst.ui.note.observeEdits
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.note.types.AudioHeader
import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader
import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.types.DisplayHighlight
import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList
import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet
import com.vitorpamplona.amethyst.ui.note.types.EditState
@ -128,6 +125,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderPoll
@ -144,8 +142,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
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.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
@ -184,7 +181,6 @@ import com.vitorpamplona.quartz.events.TextNoteModificationEvent
import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -225,7 +221,10 @@ fun RenderThreadFeed(
LaunchedEffect(noteId) {
// waits to load the thread to scroll to item.
delay(100)
val noteForPosition = state.feed.value.filter { it.idHex == noteId }.firstOrNull()
val noteForPosition =
state.feed.value
.filter { it.idHex == noteId }
.firstOrNull()
var position = state.feed.value.indexOf(noteForPosition)
if (position >= 0) {
@ -321,347 +320,314 @@ fun Modifier.drawReplyLevel(
}
return@drawBehind
}
.padding(start = (2 + (level * 3)).dp)
}.padding(start = (2 + (level * 3)).dp)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NoteMaster(
baseNote: Note,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note
val noteReportsState by baseNote.live().reports.observeAsState()
val noteForReports = noteReportsState?.note ?: return
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
var showHiddenNote by remember { mutableStateOf(false) }
val context = LocalContext.current
val moreActionsExpanded = remember { mutableStateOf(false) }
val enablePopup = remember { { moreActionsExpanded.value = true } }
val noteEvent = note?.event
var popupExpanded by remember { mutableStateOf(false) }
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
if (noteEvent == null) {
BlankNote()
} else if (!account.isAcceptable(noteForReports) && !showHiddenNote) {
val reports = remember { account.getRelevantReports(noteForReports).toImmutableSet() }
HiddenNote(
reports,
note.author?.let { account.isHidden(it) } ?: false,
accountViewModel,
Modifier.fillMaxWidth(),
nav,
onClick = { showHiddenNote = true },
)
} else {
Column(
modifier
.fillMaxWidth()
.padding(top = 10.dp),
) {
val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel)
Row(
modifier =
Modifier
.padding(start = 12.dp, end = 12.dp)
.clickable(onClick = { note.author?.let { nav("User/${it.pubkeyHex}") } }),
) {
NoteAuthorPicture(
baseNote = baseNote,
nav = nav,
accountViewModel = accountViewModel,
size = 55.dp,
)
Column(modifier = Modifier.padding(start = 10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
val isCommunityPost by
remember(baseNote) {
derivedStateOf {
baseNote.event?.isTaggedAddressableKind(CommunityDefinitionEvent.KIND) == true
}
}
if (isCommunityPost) {
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
} else {
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
}
if (editState.value is GenericLoadable.Loaded) {
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
DisplayEditStatus(it)
}
}
Text(
timeAgo(note.createdAt(), context = context),
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
)
IconButton(
modifier = Size24Modifier,
onClick = enablePopup,
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(id = R.string.more_options),
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.placeholderText,
)
NoteDropDownMenu(baseNote, moreActionsExpanded, editState, accountViewModel, nav)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
ObserveDisplayNip05Status(
baseNote,
remember { Modifier.weight(1f) },
accountViewModel,
nav,
)
val geo = remember { noteEvent.getGeoHash() }
if (geo != null) {
DisplayLocation(geo, nav)
}
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
if (baseReward != null) {
DisplayReward(baseReward, baseNote, accountViewModel, nav)
}
val pow = remember { noteEvent.getPoWRank() }
if (pow > 20) {
DisplayPoW(pow)
}
if (note.isDraft()) {
DisplayDraft()
}
DisplayOtsIfInOriginal(note, editState, accountViewModel)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = note)
} else if (noteEvent is LongTextNoteEvent) {
RenderLongFormHeaderForThread(noteEvent)
} else if (noteEvent is WikiNoteEvent) {
RenderWikiHeaderForThread(noteEvent, accountViewModel, nav)
} else if (noteEvent is ClassifiedsEvent) {
RenderClassifiedsReaderForThread(noteEvent, note, accountViewModel, nav)
}
Row(
modifier =
Modifier
.padding(horizontal = 12.dp)
.combinedClickable(
onClick = {},
onLongClick = { popupExpanded = true },
),
) {
Column {
val canPreview =
note.author == account.userProfile() ||
(note.author?.let { account.userProfile().isFollowingCached(it) } ?: true) ||
!noteForReports.hasAnyReports()
if (
(noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) &&
note.channelHex() != null
) {
ChannelHeader(
channelHex = note.channelHex()!!,
showVideo = true,
sendToChannel = true,
accountViewModel = accountViewModel,
nav = nav,
)
} else if (noteEvent is VideoEvent) {
VideoDisplay(baseNote, false, true, backgroundColor, false, accountViewModel, nav)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote, true, false, accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote, true, false, accountViewModel)
} else if (noteEvent is PeopleListEvent) {
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AudioTrackEvent) {
AudioTrackHeader(noteEvent, baseNote, false, accountViewModel, nav)
} else if (noteEvent is AudioHeaderEvent) {
AudioHeader(noteEvent, baseNote, false, accountViewModel, nav)
} else if (noteEvent is CommunityPostApprovalEvent) {
RenderPostApproval(
baseNote,
quotesLeft = 3,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is PinListEvent) {
RenderPinListEvent(
baseNote,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is EmojiPackEvent) {
RenderEmojiPack(
baseNote,
true,
backgroundColor,
accountViewModel,
)
} else if (noteEvent is RelaySetEvent) {
DisplayRelaySet(
baseNote,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is FhirResourceEvent) {
RenderFhirResource(baseNote, accountViewModel, nav)
} else if (noteEvent is GitRepositoryEvent) {
RenderGitRepositoryEvent(baseNote, accountViewModel, nav)
} else if (noteEvent is GitPatchEvent) {
RenderGitPatchEvent(baseNote, false, true, quotesLeft = 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is GitIssueEvent) {
RenderGitIssueEvent(baseNote, false, true, quotesLeft = 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AppDefinitionEvent) {
RenderAppDefinition(baseNote, accountViewModel, nav)
} else if (noteEvent is DraftEvent) {
RenderDraft(baseNote, 3, true, backgroundColor, accountViewModel, nav)
} else if (noteEvent is HighlightEvent) {
DisplayHighlight(
noteEvent.quote(),
noteEvent.author(),
noteEvent.inUrl(),
noteEvent.inPost(),
false,
true,
quotesLeft = 3,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
RenderRepost(baseNote, quotesLeft = 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is TextNoteModificationEvent) {
RenderTextModificationEvent(
note = baseNote,
makeItShort = false,
canPreview = true,
quotesLeft = 3,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is PollNoteEvent) {
RenderPoll(
baseNote,
false,
canPreview,
quotesLeft = 3,
unPackReply = false,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is PrivateDmEvent) {
RenderPrivateMessage(
baseNote,
false,
canPreview,
3,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is ChannelMessageEvent) {
RenderChannelMessage(
baseNote,
false,
canPreview,
3,
backgroundColor,
editState,
accountViewModel,
nav,
)
} else if (noteEvent is LiveActivitiesChatMessageEvent) {
RenderLiveActivityChatMessage(
baseNote,
false,
canPreview,
3,
backgroundColor,
editState,
accountViewModel,
nav,
)
} else {
RenderTextEvent(
baseNote,
false,
canPreview,
quotesLeft = 3,
unPackReply = false,
backgroundColor,
editState,
accountViewModel,
nav,
)
}
}
}
val noteEvent = baseNote.event
val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false }
if (zapSplits && noteEvent != null) {
Spacer(modifier = DoubleVertSpacer)
Row(
modifier = Modifier.padding(horizontal = 12.dp),
) {
DisplayZapSplits(noteEvent, false, accountViewModel, nav)
}
}
ReactionsRow(note, true, true, editState, accountViewModel, nav)
}
NoteQuickActionMenu(
note = note,
popupExpanded = popupExpanded,
onDismiss = { popupExpanded = false },
onWantsToEditDraft = { },
WatchNoteEvent(
baseNote = baseNote,
accountViewModel = accountViewModel,
modifier,
) {
CheckHiddenFeedWatchBlockAndReport(
note = baseNote,
modifier = modifier,
ignoreAllBlocksAndReports = false,
showHiddenWarning = true,
accountViewModel = accountViewModel,
nav = nav,
) { canPreview ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
FullBleedNoteCompose(
baseNote,
modifier
.combinedClickable(
onClick = {},
onLongClick = showPopup,
),
canPreview,
parentBackgroundColor = parentBackgroundColor,
accountViewModel,
nav,
)
}
}
}
}
@Composable
private fun FullBleedNoteCompose(
baseNote: Note,
modifier: Modifier,
canPreview: Boolean,
parentBackgroundColor: MutableState<Color>?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val noteEvent = baseNote.event ?: return
val backgroundColor =
calculateBackgroundColor(
baseNote.createdAt(),
null,
parentBackgroundColor,
accountViewModel,
)
Column(
Modifier
.fillMaxWidth()
.padding(top = 10.dp),
) {
val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel)
Row(
modifier =
Modifier
.padding(start = 12.dp, end = 12.dp)
.clickable(onClick = { baseNote.author?.let { nav(routeFor(it)) } }),
) {
NoteAuthorPicture(
baseNote = baseNote,
nav = nav,
accountViewModel = accountViewModel,
size = Size55dp,
)
Column(modifier = Modifier.padding(start = 10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
val isCommunityPost by
remember(baseNote) {
derivedStateOf {
baseNote.event?.isTaggedAddressableKind(CommunityDefinitionEvent.KIND) == true
}
}
if (isCommunityPost) {
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
} else {
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
}
if (editState.value is GenericLoadable.Loaded) {
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
DisplayEditStatus(it)
}
}
TimeAgo(note = baseNote)
MoreOptionsButton(baseNote, editState, accountViewModel, nav)
}
Row(verticalAlignment = Alignment.CenterVertically) {
ObserveDisplayNip05Status(
baseNote,
remember { Modifier.weight(1f) },
accountViewModel,
nav,
)
val geo = remember { noteEvent.getGeoHash() }
if (geo != null) {
DisplayLocation(geo, nav)
}
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
if (baseReward != null) {
DisplayReward(baseReward, baseNote, accountViewModel, nav)
}
val pow = remember { noteEvent.getPoWRank() }
if (pow > 20) {
DisplayPoW(pow)
}
if (baseNote.isDraft()) {
DisplayDraft()
}
DisplayOtsIfInOriginal(baseNote, editState, accountViewModel)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = baseNote)
} else if (noteEvent is LongTextNoteEvent) {
RenderLongFormHeaderForThread(noteEvent)
} else if (noteEvent is WikiNoteEvent) {
RenderWikiHeaderForThread(noteEvent, accountViewModel, nav)
} else if (noteEvent is ClassifiedsEvent) {
RenderClassifiedsReaderForThread(noteEvent, baseNote, accountViewModel, nav)
}
Row(
modifier =
modifier
.padding(horizontal = 12.dp),
) {
Column {
if (
(noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) &&
baseNote.channelHex() != null
) {
ChannelHeader(
channelHex = baseNote.channelHex()!!,
showVideo = true,
sendToChannel = true,
accountViewModel = accountViewModel,
nav = nav,
)
} else if (noteEvent is VideoEvent) {
VideoDisplay(baseNote, false, true, backgroundColor, false, accountViewModel, nav)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote, true, false, accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote, true, false, accountViewModel)
} else if (noteEvent is PeopleListEvent) {
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AudioTrackEvent) {
AudioTrackHeader(noteEvent, baseNote, false, accountViewModel, nav)
} else if (noteEvent is AudioHeaderEvent) {
AudioHeader(noteEvent, baseNote, false, accountViewModel, nav)
} else if (noteEvent is CommunityPostApprovalEvent) {
RenderPostApproval(
baseNote,
quotesLeft = 3,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is PinListEvent) {
RenderPinListEvent(
baseNote,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is EmojiPackEvent) {
RenderEmojiPack(
baseNote,
true,
backgroundColor,
accountViewModel,
)
} else if (noteEvent is RelaySetEvent) {
DisplayRelaySet(
baseNote,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is FhirResourceEvent) {
RenderFhirResource(baseNote, accountViewModel, nav)
} else if (noteEvent is GitRepositoryEvent) {
RenderGitRepositoryEvent(baseNote, accountViewModel, nav)
} else if (noteEvent is GitPatchEvent) {
RenderGitPatchEvent(baseNote, false, true, quotesLeft = 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is GitIssueEvent) {
RenderGitIssueEvent(baseNote, false, true, quotesLeft = 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AppDefinitionEvent) {
RenderAppDefinition(baseNote, accountViewModel, nav)
} else if (noteEvent is DraftEvent) {
RenderDraft(baseNote, 3, true, backgroundColor, accountViewModel, nav)
} else if (noteEvent is HighlightEvent) {
RenderHighlight(baseNote, false, canPreview, quotesLeft = 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
RenderRepost(baseNote, quotesLeft = 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is TextNoteModificationEvent) {
RenderTextModificationEvent(
note = baseNote,
makeItShort = false,
canPreview = true,
quotesLeft = 3,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is PollNoteEvent) {
RenderPoll(
baseNote,
false,
canPreview,
quotesLeft = 3,
unPackReply = false,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is PrivateDmEvent) {
RenderPrivateMessage(
baseNote,
false,
canPreview,
3,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is ChannelMessageEvent) {
RenderChannelMessage(
baseNote,
false,
canPreview,
3,
backgroundColor,
editState,
accountViewModel,
nav,
)
} else if (noteEvent is LiveActivitiesChatMessageEvent) {
RenderLiveActivityChatMessage(
baseNote,
false,
canPreview,
3,
backgroundColor,
editState,
accountViewModel,
nav,
)
} else {
RenderTextEvent(
baseNote,
false,
canPreview,
quotesLeft = 3,
unPackReply = false,
backgroundColor,
editState,
accountViewModel,
nav,
)
}
}
}
val noteEvent = baseNote.event
val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false }
if (zapSplits && noteEvent != null) {
Spacer(modifier = DoubleVertSpacer)
Row(
modifier = Modifier.padding(horizontal = 12.dp),
) {
DisplayZapSplits(noteEvent, false, accountViewModel, nav)
}
}
ReactionsRow(baseNote, true, true, editState, accountViewModel, nav)
}
}

View File

@ -254,16 +254,70 @@ class AccountViewModel(
}
}
val noteIsHiddenFlows = LruCache<Note, StateFlow<Boolean>>(300)
@Immutable
data class NoteComposeReportState(
val isPostHidden: Boolean = false,
val isAcceptable: Boolean = true,
val canPreview: Boolean = true,
val isHiddenAuthor: Boolean = false,
val relevantReports: ImmutableSet<Note> = persistentSetOf(),
)
fun createIsHiddenFlow(note: Note): StateFlow<Boolean> =
fun isNoteAcceptable(
note: Note,
accountChoices: Account.LiveHiddenUsers,
followUsers: Set<HexKey>,
): NoteComposeReportState {
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
val isFromLoggedInFollow = note.author?.let { followUsers.contains(it.pubkeyHex) } ?: true
val isPostHidden = note.isHiddenFor(accountChoices)
val isHiddenAuthor = note.author?.let { account.isHidden(it) } == true
return if (isPostHidden) {
// Spam + Blocked Users + Hidden Words + Sensitive Content
NoteComposeReportState(isPostHidden, false, false, isHiddenAuthor, persistentSetOf())
} else if (isFromLoggedIn || isFromLoggedInFollow) {
// No need to process if from trusted people
NoteComposeReportState(isPostHidden, true, true, isHiddenAuthor, persistentSetOf())
} else {
val newCanPreview = !note.hasAnyReports()
val newIsAcceptable = account.isAcceptable(note)
if (newCanPreview && newIsAcceptable) {
// No need to process reports if nothing is wrong
NoteComposeReportState(isPostHidden, true, true, false, persistentSetOf())
} else {
val newRelevantReports = account.getRelevantReports(note)
NoteComposeReportState(
isPostHidden,
newIsAcceptable,
newCanPreview,
false,
newRelevantReports.toImmutableSet(),
)
}
}
}
val noteIsHiddenFlows = LruCache<Note, StateFlow<NoteComposeReportState>>(300)
fun createIsHiddenFlow(note: Note): StateFlow<NoteComposeReportState> =
noteIsHiddenFlows.get(note)
?: combineTransform(account.flowHiddenUsers, note.flow().metadata.stateFlow) { hiddenUsers, metadata ->
emit(metadata.note.isHiddenFor(hiddenUsers))
?: combineTransform(
account.flowHiddenUsers,
account.liveKind3FollowsFlow,
note.flow().metadata.stateFlow,
note.flow().reports.stateFlow,
) { hiddenUsers, followingUsers, metadata, reports ->
emit(
isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users),
)
}.stateIn(
viewModelScope,
SharingStarted.Eagerly,
false,
NoteComposeReportState(),
).also {
noteIsHiddenFlows.put(note, it)
}
@ -761,52 +815,6 @@ class AccountViewModel(
fun defaultZapType(): LnZapEvent.ZapType = account.defaultZapType
@Immutable
data class NoteComposeReportState(
val isAcceptable: Boolean = true,
val canPreview: Boolean = true,
val isHiddenAuthor: Boolean = false,
val relevantReports: ImmutableSet<Note> = persistentSetOf(),
)
suspend fun isNoteAcceptable(
note: Note,
onReady: (NoteComposeReportState) -> Unit,
) {
val newState =
withContext(Dispatchers.IO) {
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
if (isFromLoggedIn || isFromLoggedInFollow) {
// No need to process if from trusted people
NoteComposeReportState(true, true, false, persistentSetOf())
} else if (note.author?.let { account.isHidden(it) } == true) {
NoteComposeReportState(false, false, true, persistentSetOf())
} else {
val newCanPreview = !note.hasAnyReports()
val newIsAcceptable = account.isAcceptable(note)
if (newCanPreview && newIsAcceptable) {
// No need to process reports if nothing is wrong
NoteComposeReportState(true, true, false, persistentSetOf())
} else {
val newRelevantReports = account.getRelevantReports(note)
NoteComposeReportState(
newIsAcceptable,
newCanPreview,
false,
newRelevantReports.toImmutableSet(),
)
}
}
}
onReady(newState)
}
fun unwrap(
event: GiftWrapEvent,
onReady: (Event) -> Unit,

View File

@ -24,6 +24,7 @@ import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
@ -51,7 +52,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
import androidx.compose.ui.Modifier
@ -70,14 +70,13 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.BoostReaction
import com.vitorpamplona.amethyst.ui.note.HiddenNote
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.RenderRelay
import com.vitorpamplona.amethyst.ui.note.ReplyReaction
import com.vitorpamplona.amethyst.ui.note.ViewCountReaction
import com.vitorpamplona.amethyst.ui.note.WatchForReports
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
@ -107,8 +106,6 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.VideoEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun VideoScreen(
@ -249,75 +246,24 @@ fun SlidingCarousel(
key = { index -> feed.value.getOrNull(index)?.idHex ?: "$index" },
) { index ->
feed.value.getOrNull(index)?.let { note ->
LoadedVideoCompose(note, showHidden, accountViewModel, nav)
}
}
}
@Composable
fun LoadedVideoCompose(
note: Note,
showHidden: Boolean,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
var state by
remember(note) {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
if (!showHidden) {
val scope = rememberCoroutineScope()
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
scope.launch(Dispatchers.Main) { state = newState }
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CheckHiddenFeedWatchBlockAndReport(
note = note,
modifier = Modifier.fillMaxWidth(),
showHiddenWarning = true,
ignoreAllBlocksAndReports = showHidden,
accountViewModel = accountViewModel,
nav = nav,
) {
RenderVideoOrPictureNote(
note,
accountViewModel,
nav,
)
}
}
}
}
Crossfade(targetState = state, label = "LoadedVideoCompose") {
RenderReportState(
it,
note,
accountViewModel,
nav,
)
}
}
@Composable
fun RenderReportState(
state: AccountViewModel.NoteComposeReportState,
note: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
var showReportedNote by remember { mutableStateOf(false) }
Crossfade(targetState = (!state.isAcceptable || state.isHiddenAuthor) && !showReportedNote) {
showHiddenNote ->
if (showHiddenNote) {
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
Modifier.fillMaxWidth(),
nav,
onClick = { showReportedNote = true },
)
}
} else {
RenderVideoOrPictureNote(
note,
accountViewModel,
nav,
)
}
}
}
@Composable
@ -478,8 +424,7 @@ fun ReactionsColumn(
routeFor(
baseNote,
accountViewModel.userProfile(),
)
?.let { nav(it) }
)?.let { nav(it) }
}
BoostReaction(
baseNote = baseNote,