Continues to move away from starting co-routines directly in composables

This commit is contained in:
Vitor Pamplona
2023-08-25 17:14:33 -04:00
parent a7d8c87c47
commit 7ef46a2662
15 changed files with 213 additions and 200 deletions
@@ -463,10 +463,6 @@ open class Note(val idHex: String) {
}.flatten() }.flatten()
} }
fun countReactions(): Int {
return reactions.values.sumOf { it.size }
}
fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal { fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
// Regular Zap Receipts // Regular Zap Receipts
val completedZaps = zaps.asSequence() val completedZaps = zaps.asSequence()
@@ -683,6 +679,10 @@ class NoteLiveSet(u: Note) {
it.note.replies.size it.note.replies.size
}.distinctUntilChanged() }.distinctUntilChanged()
val reactionCount = reactions.map {
it.note.reactions.values.sumOf { it.size }
}.distinctUntilChanged()
val boostCount = boosts.map { val boostCount = boosts.map {
it.note.boosts.size it.note.boosts.size
}.distinctUntilChanged() }.distinctUntilChanged()
@@ -111,12 +111,14 @@ abstract class NostrDataSource(val debugName: String) {
private val bundler = BundledUpdate(300, Dispatchers.IO) private val bundler = BundledUpdate(300, Dispatchers.IO)
fun invalidateFilters() { fun invalidateFilters() {
bundler.invalidate() { scope.launch(Dispatchers.IO) {
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") bundler.invalidate() {
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
// adds the time to perform the refresh into this delay // adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines. // holding off new updates in case of heavy refresh routines.
resetFiltersSuspend() resetFiltersSuspend()
}
} }
} }
@@ -12,8 +12,6 @@ import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) { fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) {
@@ -37,10 +35,8 @@ fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel)
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created). // Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
if (urlPreviewState == UrlPreviewState.Loading) { if (urlPreviewState == UrlPreviewState.Loading) {
LaunchedEffect(url) { LaunchedEffect(url) {
launch(Dispatchers.IO) { accountViewModel.urlPreview(url) {
UrlCachedPreviewer.previewInfo(url) { urlPreviewState = it
urlPreviewState = it
}
} }
} }
} }
@@ -285,20 +285,16 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
), ),
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onSend = { onSend = {
scope.launch(Dispatchers.IO) { accountViewModel.createStatus(currentStatus.value)
accountViewModel.createStatus(currentStatus.value) focusManager.clearFocus(true)
focusManager.clearFocus(true)
}
} }
), ),
singleLine = true, singleLine = true,
trailingIcon = { trailingIcon = {
if (hasChanged) { if (hasChanged) {
UserStatusSendButton() { UserStatusSendButton() {
scope.launch(Dispatchers.IO) { accountViewModel.createStatus(currentStatus.value)
accountViewModel.createStatus(currentStatus.value) focusManager.clearFocus(true)
focusManager.clearFocus(true)
}
} }
} }
} }
@@ -335,28 +331,21 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
), ),
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onSend = { onSend = {
scope.launch(Dispatchers.IO) { accountViewModel.updateStatus(it, thisStatus.value)
accountViewModel.updateStatus(it, thisStatus.value) focusManager.clearFocus(true)
focusManager.clearFocus(true)
}
} }
), ),
singleLine = true, singleLine = true,
trailingIcon = { trailingIcon = {
if (hasChanged) { if (hasChanged) {
UserStatusSendButton() { UserStatusSendButton() {
scope.launch(Dispatchers.IO) { accountViewModel.updateStatus(it, thisStatus.value)
accountViewModel.updateStatus(it, thisStatus.value) focusManager.clearFocus(true)
focusManager.clearFocus(true)
}
} }
} else { } else {
UserStatusDeleteButton() { UserStatusDeleteButton() {
scope.launch(Dispatchers.IO) { accountViewModel.deleteStatus(it)
accountViewModel.updateStatus(it, "") focusManager.clearFocus(true)
accountViewModel.delete(it)
focusManager.clearFocus(true)
}
} }
} }
} }
@@ -42,7 +42,6 @@ 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 com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
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.ui.components.ImageUrlType import com.vitorpamplona.amethyst.ui.components.ImageUrlType
@@ -52,7 +51,6 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.CombinedZap import com.vitorpamplona.amethyst.ui.screen.CombinedZap
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
@@ -70,8 +68,6 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -352,8 +348,7 @@ private fun ParseAuthorCommentAndAmount(
} }
LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) { LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) {
launch(Dispatchers.IO) { accountViewModel.decryptAmountMessage(zapRequest, zapEvent) { newState ->
val newState = loadAmountState(zapRequest, zapEvent, accountViewModel)
if (newState != null) { if (newState != null) {
content.value = newState content.value = newState
} }
@@ -363,34 +358,6 @@ private fun ParseAuthorCommentAndAmount(
onReady(content) onReady(content)
} }
private suspend fun loadAmountState(
zapRequest: Note,
zapEvent: Note?,
accountViewModel: AccountViewModel
): ZapAmountCommentNotification? {
(zapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = accountViewModel.decryptZap(zapRequest)
val amount = (zapEvent?.event as? LnZapEvent)?.amount
if (decryptedContent != null) {
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
return ZapAmountCommentNotification(
newAuthor,
decryptedContent.content.ifBlank { null },
showAmountAxis(amount)
)
} else {
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
return ZapAmountCommentNotification(
zapRequest.author,
zapRequest.event?.content()?.ifBlank { null },
showAmountAxis(amount)
)
}
}
}
return null
}
@Composable @Composable
private fun RenderState( private fun RenderState(
content: MutableState<ZapAmountCommentNotification>, content: MutableState<ZapAmountCommentNotification>,
@@ -98,9 +98,7 @@ private fun WatchZapsAndUpdateTallies(
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
launch(Dispatchers.Default) { pollViewModel.refreshTallies()
pollViewModel.refreshTallies()
}
} }
} }
@@ -3,13 +3,16 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
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.quartz.events.* import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
@@ -52,12 +55,12 @@ class PollNoteViewModel : ViewModel() {
closedAt = pollEvent?.getTagInt(CLOSED_AT) closedAt = pollEvent?.getTagInt(CLOSED_AT)
} }
suspend fun refreshTallies() { fun refreshTallies() {
totalZapped = totalZapped() viewModelScope.launch(Dispatchers.Default) {
wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false totalZapped = totalZapped()
wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
_tallies.emit( val newOptions = pollOptions?.keys?.map {
pollOptions?.keys?.map {
val zappedInOption = zappedPollOptionAmount(it) val zappedInOption = zappedPollOptionAmount(it)
val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) {
@@ -71,8 +74,12 @@ class PollNoteViewModel : ViewModel() {
val consensus = consensusThreshold != null && myTally >= consensusThreshold!! val consensus = consensusThreshold != null && myTally >= consensusThreshold!!
PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn) PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn)
} ?: emptyList() }
)
_tallies.emit(
newOptions ?: emptyList()
)
}
} }
fun canZap(): Boolean { fun canZap(): Boolean {
@@ -40,6 +40,7 @@ import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
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.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@@ -241,7 +242,7 @@ private fun LoadAndDisplayZapraiser(
wantsToSeeReactions: MutableState<Boolean>, wantsToSeeReactions: MutableState<Boolean>,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
val zapraiserAmount by remember { val zapraiserAmount by remember(baseNote) {
derivedStateOf { derivedStateOf {
baseNote.event?.zapraiserAmount() ?: 0 baseNote.event?.zapraiserAmount() ?: 0
} }
@@ -261,42 +262,26 @@ private fun LoadAndDisplayZapraiser(
} }
} }
@Immutable
data class ZapraiserStatus(val progress: Float, val left: String)
@Composable @Composable
fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, accountViewModel: AccountViewModel) { fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, accountViewModel: AccountViewModel) {
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
var zapraiserProgress by remember { mutableStateOf(0F) } var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) }
var zapraiserLeft by remember { mutableStateOf("$zapraiserAmount") }
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
launch(Dispatchers.Default) { zapsState?.note?.let {
zapsState?.note?.let { accountViewModel.calculateZapraiser(baseNote) { newStatus ->
val newZapAmount = accountViewModel.calculateZapAmount(it) if (zapraiserStatus != newStatus) {
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat() zapraiserStatus = newStatus
if (percentage > 1) {
percentage = 1f
}
if (Math.abs(zapraiserProgress - percentage) > 0.001) {
val newZapraiserProgress = percentage
val newZapraiserLeft = if (percentage > 0.99) {
"0"
} else {
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
}
if (zapraiserLeft != newZapraiserLeft) {
zapraiserLeft = newZapraiserLeft
}
if (zapraiserProgress != newZapraiserProgress) {
zapraiserProgress = newZapraiserProgress
}
} }
} }
} }
} }
val color = if (zapraiserProgress > 0.99) { val color = if (zapraiserStatus.progress > 0.99) {
DarkerGreen DarkerGreen
} else { } else {
MaterialTheme.colors.mediumImportanceLink MaterialTheme.colors.mediumImportanceLink
@@ -307,7 +292,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
.fillMaxWidth() .fillMaxWidth()
.height(if (details) 24.dp else 4.dp), .height(if (details) 24.dp else 4.dp),
color = color, color = color,
progress = zapraiserProgress progress = zapraiserStatus.progress
) )
if (details) { if (details) {
@@ -315,14 +300,14 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
contentAlignment = Center, contentAlignment = Center,
modifier = TinyBorders modifier = TinyBorders
) { ) {
val totalPercentage by remember(zapraiserProgress) { val totalPercentage by remember(zapraiserStatus) {
derivedStateOf { derivedStateOf {
"${(zapraiserProgress * 100).roundToInt()}%" "${(zapraiserStatus.progress * 100).roundToInt()}%"
} }
} }
Text( Text(
text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserLeft), text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserStatus.left),
modifier = NoSoTinyBorders, modifier = NoSoTinyBorders,
color = MaterialTheme.colors.placeholderText, color = MaterialTheme.colors.placeholderText,
fontSize = Font14SP, fontSize = Font14SP,
@@ -836,10 +821,7 @@ private fun WatchReactionTypeForNote(baseNote: Note, accountViewModel: AccountVi
val reactionsState by baseNote.live().reactions.observeAsState() val reactionsState by baseNote.live().reactions.observeAsState()
LaunchedEffect(key1 = reactionsState) { LaunchedEffect(key1 = reactionsState) {
launch(Dispatchers.Default) { accountViewModel.loadReactionTo(reactionsState?.note, onNewReactionType)
val reactionNote = reactionsState?.note?.getReactionBy(accountViewModel.userProfile())
onNewReactionType(reactionNote)
}
} }
} }
@@ -874,32 +856,9 @@ private fun RenderReactionType(
@Composable @Composable
fun LikeText(baseNote: Note, grayTint: Color) { fun LikeText(baseNote: Note, grayTint: Color) {
val reactionsCount = remember(baseNote) { val reactionCount by baseNote.live().reactionCount.observeAsState(0)
mutableStateOf(baseNote.reactions.size)
}
val scope = rememberCoroutineScope() SlidingAnimationCount(reactionCount, grayTint)
WatchReactionCountForNote(baseNote) { newReactionsCount ->
if (reactionsCount.value != newReactionsCount) {
scope.launch(Dispatchers.Main) {
reactionsCount.value = newReactionsCount
}
}
}
SlidingAnimationCount(reactionsCount, grayTint)
}
@Composable
private fun WatchReactionCountForNote(baseNote: Note, onNewReactionCount: (Int) -> Unit) {
val reactionsState by baseNote.live().reactions.observeAsState()
LaunchedEffect(key1 = reactionsState) {
launch(Dispatchers.Default) {
onNewReactionCount(reactionsState?.note?.countReactions() ?: 0)
}
}
} }
private fun likeClick( private fun likeClick(
@@ -1139,9 +1098,7 @@ private fun WatchZapsForNote(baseNote: Note, accountViewModel: AccountViewModel,
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
launch(Dispatchers.Default) { accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, onWasZapped)
onWasZapped(accountViewModel.calculateIfNoteWasZappedByAccount(baseNote))
}
} }
} }
@@ -1171,9 +1128,7 @@ fun WatchZapAmountsForNote(baseNote: Note, accountViewModel: AccountViewModel, o
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
launch(Dispatchers.Default) { accountViewModel.calculateZapAmount(baseNote, onZapAmount)
onZapAmount(showAmount(accountViewModel.calculateZapAmount(baseNote)))
}
} }
} }
@@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -102,11 +101,9 @@ private fun WatchScrollToTop(
val scrollToTop by viewModel.scrollToTop.collectAsState() val scrollToTop by viewModel.scrollToTop.collectAsState()
LaunchedEffect(scrollToTop) { LaunchedEffect(scrollToTop) {
launch { if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
if (scrollToTop > 0 && viewModel.scrolltoTopPending) { listState.scrollToItem(index = 0)
listState.scrollToItem(index = 0) viewModel.sentToTop()
viewModel.sentToTop()
}
} }
} }
} }
@@ -301,11 +301,13 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
fun checkKeysInvalidateDataAndSendToTop() { fun checkKeysInvalidateDataAndSendToTop() {
if (lastFeedKey != localFilter.feedKey()) { if (lastFeedKey != localFilter.feedKey()) {
bundler.invalidate(false) { viewModelScope.launch(Dispatchers.IO) {
// adds the time to perform the refresh into this delay bundler.invalidate(false) {
// holding off new updates in case of heavy refresh routines. // adds the time to perform the refresh into this delay
refreshSuspended() // holding off new updates in case of heavy refresh routines.
sendToTop() refreshSuspended()
sendToTop()
}
} }
} }
} }
@@ -16,14 +16,22 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountState import com.vitorpamplona.amethyst.model.AccountState
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.SealedGossipEvent
@@ -121,21 +129,86 @@ class AccountViewModel(val account: Account) : ViewModel() {
account.delete(account.boostsTo(note)) account.delete(account.boostsTo(note))
} }
fun calculateIfNoteWasZappedByAccount(zappedNote: Note): Boolean { fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) {
return account.calculateIfNoteWasZappedByAccount(zappedNote) viewModelScope.launch(Dispatchers.Default) {
onWasZapped(account.calculateIfNoteWasZappedByAccount(zappedNote))
}
} }
fun calculateZapAmount(zappedNote: Note): BigDecimal { suspend fun calculateZapAmount(zappedNote: Note): BigDecimal {
return account.calculateZappedAmount(zappedNote) return account.calculateZappedAmount(zappedNote)
} }
fun calculateZapAmount(zappedNote: Note, onZapAmount: (String) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onZapAmount(showAmount(account.calculateZappedAmount(zappedNote)))
}
}
fun calculateZapraiser(zappedNote: Note, onZapraiserStatus: (ZapraiserStatus) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0
val newZapAmount = calculateZapAmount(zappedNote)
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
if (percentage > 1) {
percentage = 1f
}
val newZapraiserProgress = percentage
val newZapraiserLeft = if (percentage > 0.99) {
"0"
} else {
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
}
onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft))
}
}
fun decryptAmountMessage(
zapRequest: Note,
zapEvent: Note?,
onNewState: (ZapAmountCommentNotification?) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
onNewState(innerDecryptAmountMessage(zapRequest, zapEvent))
}
}
private suspend fun innerDecryptAmountMessage(
zapRequest: Note,
zapEvent: Note?
): ZapAmountCommentNotification? {
(zapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = decryptZap(zapRequest)
val amount = (zapEvent?.event as? LnZapEvent)?.amount
if (decryptedContent != null) {
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
return ZapAmountCommentNotification(
newAuthor,
decryptedContent.content.ifBlank { null },
showAmountAxis(amount)
)
} else {
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
return ZapAmountCommentNotification(
zapRequest.author,
zapRequest.event?.content()?.ifBlank { null },
showAmountAxis(amount)
)
}
}
}
return null
}
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) { fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
innerZap(note, amount, pollOption, message, context, onError, onProgress, zapType) innerZap(note, amount, pollOption, message, context, onError, onProgress, zapType)
} }
} }
suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) { private suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim() val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
if (lud16.isNullOrBlank()) { if (lud16.isNullOrBlank()) {
@@ -388,6 +461,26 @@ class AccountViewModel(val account: Account) : ViewModel() {
fun updateStatus(it: AddressableNote, newStatus: String) { fun updateStatus(it: AddressableNote, newStatus: String) {
account.updateStatus(it, newStatus) account.updateStatus(it, newStatus)
fun checkIfOnline(url: String, onResult: (Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val isOnline = OnlineChecker.isOnline(url)
onResult(isOnline)
}
}
fun urlPreview(url: String, onResult: suspend (UrlPreviewState) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
UrlCachedPreviewer.previewInfo(url, onResult)
}
}
fun loadReactionTo(note: Note?, onNewReactionType: (String?) -> Unit) {
if (note == null) return
viewModelScope.launch(Dispatchers.Default) {
onNewReactionType(note.getReactionBy(userProfile()))
}
} }
class Factory(val account: Account) : ViewModelProvider.Factory { class Factory(val account: Account) : ViewModelProvider.Factory {
@@ -623,7 +623,7 @@ fun ShowVideoStreaming(
} }
url?.let { url?.let {
CheckIfUrlIsOnline(url) { CheckIfUrlIsOnline(url, accountViewModel) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = remember { Modifier.heightIn(max = 300.dp) } modifier = remember { Modifier.heightIn(max = 300.dp) }
@@ -29,7 +29,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
@@ -42,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -136,12 +134,14 @@ private fun HomePages(
} }
@Composable @Composable
fun CheckIfUrlIsOnline(url: String, whenOnline: @Composable () -> Unit) { fun CheckIfUrlIsOnline(url: String, accountViewModel: AccountViewModel, whenOnline: @Composable () -> Unit) {
var online by remember { mutableStateOf(false) } var online by remember { mutableStateOf(false) }
LaunchedEffect(key1 = url) { LaunchedEffect(key1 = url) {
launch(Dispatchers.IO) { accountViewModel.checkIfOnline(url) { isOnline ->
online = OnlineChecker.isOnline(url) if (online != isOnline) {
online = isOnline
}
} }
} }
@@ -162,11 +162,9 @@ fun WatchAccountForHomeScreen(
val followState by accountViewModel.account.userProfile().live().follows.observeAsState() val followState by accountViewModel.account.userProfile().live().follows.observeAsState()
LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList, followState) { LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList, followState) {
launch(Dispatchers.IO) { NostrHomeDataSource.invalidateFilters()
NostrHomeDataSource.invalidateFilters() homeFeedViewModel.checkKeysInvalidateDataAndSendToTop()
homeFeedViewModel.checkKeysInvalidateDataAndSendToTop() repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop()
repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop()
}
} }
} }
@@ -73,42 +73,38 @@ fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, nav
fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = noteState) { LaunchedEffect(key1 = noteState) {
scope.launch { val note = noteState?.note ?: return@LaunchedEffect
val note = noteState?.note ?: return@launch var event = note.event
var event = note.event val channelHex = note.channelHex()
val channelHex = note.channelHex()
if (event is GiftWrapEvent) { if (event is GiftWrapEvent) {
event = accountViewModel.unwrap(event) event = accountViewModel.unwrap(event)
} }
if (event is SealedGossipEvent) { if (event is SealedGossipEvent) {
event = accountViewModel.unseal(event) event = accountViewModel.unseal(event)
} }
if (event == null) { if (event == null) {
// stay here, loading // stay here, loading
} else if (event is ChannelCreateEvent) { } else if (event is ChannelCreateEvent) {
nav("Channel/${note.idHex}") nav("Channel/${note.idHex}")
} else if (event is ChatroomKeyable) { } else if (event is ChatroomKeyable) {
note.author?.let { note.author?.let {
val withKey = (event as ChatroomKeyable) val withKey = (event as ChatroomKeyable)
.chatroomKey(accountViewModel.userProfile().pubkeyHex) .chatroomKey(accountViewModel.userProfile().pubkeyHex)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
accountViewModel.userProfile().createChatroom(withKey) accountViewModel.userProfile().createChatroom(withKey)
}
nav("Room/${withKey.hashCode()}")
} }
} else if (channelHex != null) {
nav("Channel/$channelHex") nav("Room/${withKey.hashCode()}")
} else {
nav("Note/${note.idHex}")
} }
} else if (channelHex != null) {
nav("Channel/$channelHex")
} else {
nav("Note/${note.idHex}")
} }
} }
@@ -50,5 +50,18 @@ class StatusEvent(
val sig = CryptoUtils.sign(id, privateKey) val sig = CryptoUtils.sign(id, privateKey)
return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, newStatus, sig.toHexKey()) return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, newStatus, sig.toHexKey())
} }
fun clear(
event: StatusEvent,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): StatusEvent {
val msg = ""
val tags = event.tags.filter { it.size > 1 && it[0] == "d" }
val pubKey = event.pubKey()
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = CryptoUtils.sign(id, privateKey)
return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
} }
} }