Moving livedata creation to long-lived objects

This commit is contained in:
Vitor Pamplona
2023-08-24 12:53:07 -04:00
parent 36ea33a919
commit 76faf7ae35
12 changed files with 95 additions and 92 deletions
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
@@ -10,6 +12,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.updated import com.vitorpamplona.amethyst.ui.actions.updated
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.combineWith
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
@@ -651,10 +654,18 @@ open class Note(val idHex: String) {
} }
} }
@Stable
class NoteLiveSet(u: Note) { class NoteLiveSet(u: Note) {
// Observers line up here. // Observers line up here.
val metadata: NoteLiveData = NoteLiveData(u) val metadata: NoteLiveData = NoteLiveData(u)
val authorChanges = metadata.map {
it.note.author
}
val hasEvent = metadata.map {
it.note.event != null
}.distinctUntilChanged()
val reactions: NoteLiveData = NoteLiveData(u) val reactions: NoteLiveData = NoteLiveData(u)
val boosts: NoteLiveData = NoteLiveData(u) val boosts: NoteLiveData = NoteLiveData(u)
val replies: NoteLiveData = NoteLiveData(u) val replies: NoteLiveData = NoteLiveData(u)
@@ -662,6 +673,20 @@ class NoteLiveSet(u: Note) {
val relays: NoteLiveData = NoteLiveData(u) val relays: NoteLiveData = NoteLiveData(u)
val zaps: NoteLiveData = NoteLiveData(u) val zaps: NoteLiveData = NoteLiveData(u)
val hasReactions = zaps.combineWith(boosts, reactions) { zapState, boostState, reactionState ->
zapState?.note?.zaps?.isNotEmpty() ?: false ||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
reactionState?.note?.reactions?.isNotEmpty() ?: false
}.distinctUntilChanged()
val replyCount = replies.map {
it.note.replies.size
}.distinctUntilChanged()
val boostCount = boosts.map {
it.note.boosts.size
}.distinctUntilChanged()
fun isInUse(): Boolean { fun isInUse(): Boolean {
return metadata.hasObservers() || return metadata.hasObservers() ||
reactions.hasObservers() || reactions.hasObservers() ||
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.EOSETime import com.vitorpamplona.amethyst.service.relays.EOSETime
@@ -378,6 +380,7 @@ class User(val pubkeyHex: String) {
} }
} }
@Stable
class UserLiveSet(u: User) { class UserLiveSet(u: User) {
// UI Observers line up here. // UI Observers line up here.
val follows: UserLiveData = UserLiveData(u) val follows: UserLiveData = UserLiveData(u)
@@ -390,6 +393,14 @@ class UserLiveSet(u: User) {
val zaps: UserLiveData = UserLiveData(u) val zaps: UserLiveData = UserLiveData(u)
val bookmarks: UserLiveData = UserLiveData(u) val bookmarks: UserLiveData = UserLiveData(u)
val profilePictureChanges = metadata.map {
it.user.profilePicture()
}.distinctUntilChanged()
val userMetadataInfo = metadata.map {
it.user.info
}.distinctUntilChanged()
fun isInUse(): Boolean { fun isInUse(): Boolean {
return follows.hasObservers() || return follows.hasObservers() ||
followers.hasObservers() || followers.hasObservers() ||
@@ -30,8 +30,6 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.em import androidx.compose.ui.unit.em
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.material.MaterialRichText import com.halilibo.richtext.ui.material.MaterialRichText
@@ -421,9 +419,7 @@ private fun ObserveNIP19Event(
@Composable @Composable
fun ObserveNote(note: Note, onRefresh: () -> Unit) { fun ObserveNote(note: Note, onRefresh: () -> Unit) {
val loadedNoteId by note.live().metadata.map { val loadedNoteId by note.live().metadata.observeAsState()
it.note.event?.id()
}.distinctUntilChanged().observeAsState(note.event?.id())
LaunchedEffect(key1 = loadedNoteId) { LaunchedEffect(key1 = loadedNoteId) {
if (loadedNoteId != null) { if (loadedNoteId != null) {
@@ -460,9 +456,7 @@ private fun ObserveNIP19User(
@Composable @Composable
private fun ObserveUser(user: User, onRefresh: () -> Unit) { private fun ObserveUser(user: User, onRefresh: () -> Unit) {
val loadedUserMetaId by user.live().metadata.map { val loadedUserMetaId by user.live().metadata.observeAsState()
it.user.info?.latestMetadata?.id
}.distinctUntilChanged().observeAsState(user.info?.latestMetadata?.id)
LaunchedEffect(key1 = loadedUserMetaId) { LaunchedEffect(key1 = loadedUserMetaId) {
if (loadedUserMetaId != null) { if (loadedUserMetaId != null) {
@@ -854,9 +848,7 @@ private fun DisplayUserFromTag(
val route = remember { "User/${baseUser.pubkeyHex}" } val route = remember { "User/${baseUser.pubkeyHex}" }
val hex = remember { baseUser.pubkeyDisplayHex() } val hex = remember { baseUser.pubkeyDisplayHex() }
val meta by baseUser.live().metadata.map { val meta by baseUser.live().userMetadataInfo.observeAsState(baseUser.info)
it.user.info
}.distinctUntilChanged().observeAsState(baseUser.info)
Crossfade(targetState = meta) { Crossfade(targetState = meta) {
Row() { Row() {
@@ -52,7 +52,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
@@ -484,7 +483,7 @@ private fun RelayStatus(
relayViewModel: RelayPoolViewModel relayViewModel: RelayPoolViewModel
) { ) {
val connectedRelaysText by relayViewModel.connectionStatus.observeAsState("--/--") val connectedRelaysText by relayViewModel.connectionStatus.observeAsState("--/--")
val isConnected by relayViewModel.isConnected.distinctUntilChanged().observeAsState(false) val isConnected by relayViewModel.isConnected.observeAsState(false)
RenderRelayStatus(connectedRelaysText, isConnected) RenderRelayStatus(connectedRelaysText, isConnected)
} }
@@ -93,24 +93,10 @@ fun ChannelCardCompose(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val isBlank by baseNote.live().metadata.map { val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
it.note.event == null
}.distinctUntilChanged().observeAsState(baseNote.event == null)
Crossfade(targetState = isBlank) { Crossfade(targetState = hasEvent) {
if (it) { if (it) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
},
false
)
}
} else {
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) { if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) {
CheckHiddenChannelCardCompose( CheckHiddenChannelCardCompose(
baseNote, baseNote,
@@ -122,6 +108,18 @@ fun ChannelCardCompose(
nav nav
) )
} }
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
},
false
)
}
} }
} }
} }
@@ -77,14 +77,12 @@ fun ChatroomHeaderCompose(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val isBlank by baseNote.live().metadata.map { val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
it.note.event == null
}.observeAsState(baseNote.event == null)
if (isBlank) { if (hasEvent) {
BlankNote(Modifier)
} else {
ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav) ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav)
} else {
BlankNote(Modifier)
} }
} }
@@ -85,12 +85,20 @@ fun ChatroomMessageCompose(
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit onWantsToReply: (Note) -> Unit
) { ) {
val isBlank by baseNote.live().metadata.map { val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
it.note.event == null
}.observeAsState(baseNote.event == null)
Crossfade(targetState = isBlank) { Crossfade(targetState = hasEvent) {
if (it) { if (it) {
CheckHiddenChatMessage(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply
)
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote( BlankNote(
remember { remember {
@@ -101,16 +109,6 @@ fun ChatroomMessageCompose(
} }
) )
} }
} else {
CheckHiddenChatMessage(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply
)
} }
} }
} }
@@ -41,8 +41,6 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
@@ -579,9 +577,7 @@ private fun WatchNoteAuthor(
baseNote: Note, baseNote: Note,
onContent: @Composable (User?) -> Unit onContent: @Composable (User?) -> Unit
) { ) {
val author by baseNote.live().metadata.map { val author by baseNote.live().authorChanges.observeAsState(baseNote.author)
it.note.author
}.observeAsState(baseNote.author)
onContent(author) onContent(author)
} }
@@ -591,9 +587,7 @@ private fun WatchUserMetadata(
author: User, author: User,
onNewMetadata: @Composable (String?) -> Unit onNewMetadata: @Composable (String?) -> Unit
) { ) {
val userProfile by author.live().metadata.map { val userProfile by author.live().profilePictureChanges.observeAsState(author.profilePicture())
it.user.profilePicture()
}.distinctUntilChanged().observeAsState(author.profilePicture())
onNewMetadata(userProfile) onNewMetadata(userProfile)
} }
@@ -223,24 +223,10 @@ fun NoteCompose(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val isBlank by baseNote.live().metadata.map { val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
it.note.event == null
}.distinctUntilChanged().observeAsState(baseNote.event == null)
Crossfade(targetState = isBlank) { Crossfade(targetState = hasEvent) {
if (it) { if (it) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
},
isBoostedNote || isQuotedNote
)
}
} else {
CheckHiddenNoteCompose( CheckHiddenNoteCompose(
note = baseNote, note = baseNote,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
@@ -255,6 +241,18 @@ fun NoteCompose(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
) )
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
},
isBoostedNote || isQuotedNote
)
}
} }
} }
} }
@@ -414,14 +414,7 @@ private fun ReactionDetailGallery(
val defaultBackgroundColor = MaterialTheme.colors.background val defaultBackgroundColor = MaterialTheme.colors.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) } val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val hasReactions by baseNote.live().zaps.combineWith( val hasReactions by baseNote.live().hasReactions.observeAsState(
baseNote.live().boosts,
baseNote.live().reactions
) { zapState, boostState, reactionState ->
zapState?.note?.zaps?.isNotEmpty() ?: false ||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
reactionState?.note?.reactions?.isNotEmpty() ?: false
}.distinctUntilChanged().observeAsState(
baseNote.zaps.isNotEmpty() || baseNote.boosts.isNotEmpty() || baseNote.reactions.isNotEmpty() baseNote.zaps.isNotEmpty() || baseNote.boosts.isNotEmpty() || baseNote.reactions.isNotEmpty()
) )
@@ -594,9 +587,7 @@ fun ReplyReaction(
@Composable @Composable
fun ReplyCounter(baseNote: Note, textColor: Color) { fun ReplyCounter(baseNote: Note, textColor: Color) {
val repliesState by baseNote.live().replies.map { val repliesState by baseNote.live().replyCount.observeAsState(baseNote.replies.size)
it.note.replies.size
}.observeAsState(baseNote.replies.size)
SlidingAnimationCount(repliesState, textColor) SlidingAnimationCount(repliesState, textColor)
} }
@@ -731,9 +722,7 @@ fun BoostIcon(baseNote: Note, iconSize: Dp = Size20dp, grayTint: Color, accountV
@Composable @Composable
fun BoostText(baseNote: Note, grayTint: Color) { fun BoostText(baseNote: Note, grayTint: Color) {
val boostState by baseNote.live().boosts.map { val boostState by baseNote.live().boostCount.observeAsState(baseNote.boosts.size)
it.note.boosts.size
}.distinctUntilChanged().observeAsState(baseNote.boosts.size)
SlidingAnimationCount(boostState, grayTint) SlidingAnimationCount(boostState, grayTint)
} }
@@ -57,6 +57,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal import java.math.BigDecimal
@@ -187,10 +188,10 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
private var takenIntoAccount = setOf<HexKey>() private var takenIntoAccount = setOf<HexKey>()
private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat() private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
val todaysReplyCount = _replies.map { showCount(it[today()]) } val todaysReplyCount = _replies.map { showCount(it[today()]) }.distinctUntilChanged()
val todaysBoostCount = _boosts.map { showCount(it[today()]) } val todaysBoostCount = _boosts.map { showCount(it[today()]) }.distinctUntilChanged()
val todaysReactionCount = _reactions.map { showCount(it[today()]) } val todaysReactionCount = _reactions.map { showCount(it[today()]) }.distinctUntilChanged()
val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) } val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) }.distinctUntilChanged()
fun formatDate(createAt: Long): String { fun formatDate(createAt: Long): String {
return sdf.format( return sdf.format(
@@ -16,5 +16,5 @@ class RelayPoolViewModel : ViewModel() {
val isConnected = RelayPool.live.map { val isConnected = RelayPool.live.map {
it.relays.connectedRelays() > 0 it.relays.connectedRelays() > 0
} }.distinctUntilChanged()
} }