diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 3a4ce8925..78b690678 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -463,10 +463,6 @@ open class Note(val idHex: String) { }.flatten() } - fun countReactions(): Int { - return reactions.values.sumOf { it.size } - } - fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal { // Regular Zap Receipts val completedZaps = zaps.asSequence() @@ -683,6 +679,10 @@ class NoteLiveSet(u: Note) { it.note.replies.size }.distinctUntilChanged() + val reactionCount = reactions.map { + it.note.reactions.values.sumOf { it.size } + }.distinctUntilChanged() + val boostCount = boosts.map { it.note.boosts.size }.distinctUntilChanged() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 54e4cac10..7bf7084f1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -111,12 +111,14 @@ abstract class NostrDataSource(val debugName: String) { private val bundler = BundledUpdate(300, Dispatchers.IO) fun invalidateFilters() { - bundler.invalidate() { - // println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") + scope.launch(Dispatchers.IO) { + bundler.invalidate() { + // println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") - // adds the time to perform the refresh into this delay - // holding off new updates in case of heavy refresh routines. - resetFiltersSuspend() + // adds the time to perform the refresh into this delay + // holding off new updates in case of heavy refresh routines. + resetFiltersSuspend() + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt index 8f88efade..b17da032e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt @@ -12,8 +12,6 @@ import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable 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). if (urlPreviewState == UrlPreviewState.Loading) { LaunchedEffect(url) { - launch(Dispatchers.IO) { - UrlCachedPreviewer.previewInfo(url) { - urlPreviewState = it - } + accountViewModel.urlPreview(url) { + urlPreviewState = it } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index beb6e17fb..a038e28ce 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -285,20 +285,16 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo ), keyboardActions = KeyboardActions( onSend = { - scope.launch(Dispatchers.IO) { - accountViewModel.createStatus(currentStatus.value) - focusManager.clearFocus(true) - } + accountViewModel.createStatus(currentStatus.value) + focusManager.clearFocus(true) } ), singleLine = true, trailingIcon = { if (hasChanged) { UserStatusSendButton() { - scope.launch(Dispatchers.IO) { - accountViewModel.createStatus(currentStatus.value) - focusManager.clearFocus(true) - } + accountViewModel.createStatus(currentStatus.value) + focusManager.clearFocus(true) } } } @@ -335,28 +331,21 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo ), keyboardActions = KeyboardActions( onSend = { - scope.launch(Dispatchers.IO) { - accountViewModel.updateStatus(it, thisStatus.value) - focusManager.clearFocus(true) - } + accountViewModel.updateStatus(it, thisStatus.value) + focusManager.clearFocus(true) } ), singleLine = true, trailingIcon = { if (hasChanged) { UserStatusSendButton() { - scope.launch(Dispatchers.IO) { - accountViewModel.updateStatus(it, thisStatus.value) - focusManager.clearFocus(true) - } + accountViewModel.updateStatus(it, thisStatus.value) + focusManager.clearFocus(true) } } else { UserStatusDeleteButton() { - scope.launch(Dispatchers.IO) { - accountViewModel.updateStatus(it, "") - accountViewModel.delete(it) - focusManager.clearFocus(true) - } + accountViewModel.deleteStatus(it) + focusManager.clearFocus(true) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 618e052bf..b5164442b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User 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.MultiSetCard 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.NotificationIconModifier 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.profile35dpModifier 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.toImmutableList import kotlinx.coroutines.Dispatchers @@ -352,8 +348,7 @@ private fun ParseAuthorCommentAndAmount( } LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) { - launch(Dispatchers.IO) { - val newState = loadAmountState(zapRequest, zapEvent, accountViewModel) + accountViewModel.decryptAmountMessage(zapRequest, zapEvent) { newState -> if (newState != null) { content.value = newState } @@ -363,34 +358,6 @@ private fun ParseAuthorCommentAndAmount( 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 private fun RenderState( content: MutableState, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 1ae831a26..c78a3bd83 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -98,9 +98,7 @@ private fun WatchZapsAndUpdateTallies( val zapsState by baseNote.live().zaps.observeAsState() LaunchedEffect(key1 = zapsState) { - launch(Dispatchers.Default) { - pollViewModel.refreshTallies() - } + pollViewModel.refreshTallies() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index e9675f200..e5cd1c436 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -3,13 +3,16 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.events.* import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import java.math.BigDecimal import java.math.RoundingMode @@ -52,12 +55,12 @@ class PollNoteViewModel : ViewModel() { closedAt = pollEvent?.getTagInt(CLOSED_AT) } - suspend fun refreshTallies() { - totalZapped = totalZapped() - wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false + fun refreshTallies() { + viewModelScope.launch(Dispatchers.Default) { + totalZapped = totalZapped() + wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false - _tallies.emit( - pollOptions?.keys?.map { + val newOptions = pollOptions?.keys?.map { val zappedInOption = zappedPollOptionAmount(it) val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { @@ -71,8 +74,12 @@ class PollNoteViewModel : ViewModel() { val consensus = consensusThreshold != null && myTally >= consensusThreshold!! PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn) - } ?: emptyList() - ) + } + + _tallies.emit( + newOptions ?: emptyList() + ) + } } fun canZap(): Boolean { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 8a2426021..ff7d3bc46 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -40,6 +40,7 @@ import androidx.compose.material.ProgressIndicatorDefaults import androidx.compose.material.Text import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf @@ -241,7 +242,7 @@ private fun LoadAndDisplayZapraiser( wantsToSeeReactions: MutableState, accountViewModel: AccountViewModel ) { - val zapraiserAmount by remember { + val zapraiserAmount by remember(baseNote) { derivedStateOf { baseNote.event?.zapraiserAmount() ?: 0 } @@ -261,42 +262,26 @@ private fun LoadAndDisplayZapraiser( } } +@Immutable +data class ZapraiserStatus(val progress: Float, val left: String) + @Composable fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, accountViewModel: AccountViewModel) { val zapsState by baseNote.live().zaps.observeAsState() - var zapraiserProgress by remember { mutableStateOf(0F) } - var zapraiserLeft by remember { mutableStateOf("$zapraiserAmount") } + var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) } LaunchedEffect(key1 = zapsState) { - launch(Dispatchers.Default) { - zapsState?.note?.let { - val newZapAmount = accountViewModel.calculateZapAmount(it) - var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat() - - 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 - } + zapsState?.note?.let { + accountViewModel.calculateZapraiser(baseNote) { newStatus -> + if (zapraiserStatus != newStatus) { + zapraiserStatus = newStatus } } } } - val color = if (zapraiserProgress > 0.99) { + val color = if (zapraiserStatus.progress > 0.99) { DarkerGreen } else { MaterialTheme.colors.mediumImportanceLink @@ -307,7 +292,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc .fillMaxWidth() .height(if (details) 24.dp else 4.dp), color = color, - progress = zapraiserProgress + progress = zapraiserStatus.progress ) if (details) { @@ -315,14 +300,14 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc contentAlignment = Center, modifier = TinyBorders ) { - val totalPercentage by remember(zapraiserProgress) { + val totalPercentage by remember(zapraiserStatus) { derivedStateOf { - "${(zapraiserProgress * 100).roundToInt()}%" + "${(zapraiserStatus.progress * 100).roundToInt()}%" } } Text( - text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserLeft), + text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserStatus.left), modifier = NoSoTinyBorders, color = MaterialTheme.colors.placeholderText, fontSize = Font14SP, @@ -836,10 +821,7 @@ private fun WatchReactionTypeForNote(baseNote: Note, accountViewModel: AccountVi val reactionsState by baseNote.live().reactions.observeAsState() LaunchedEffect(key1 = reactionsState) { - launch(Dispatchers.Default) { - val reactionNote = reactionsState?.note?.getReactionBy(accountViewModel.userProfile()) - onNewReactionType(reactionNote) - } + accountViewModel.loadReactionTo(reactionsState?.note, onNewReactionType) } } @@ -874,32 +856,9 @@ private fun RenderReactionType( @Composable fun LikeText(baseNote: Note, grayTint: Color) { - val reactionsCount = remember(baseNote) { - mutableStateOf(baseNote.reactions.size) - } + val reactionCount by baseNote.live().reactionCount.observeAsState(0) - val scope = rememberCoroutineScope() - - 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) - } - } + SlidingAnimationCount(reactionCount, grayTint) } private fun likeClick( @@ -1139,9 +1098,7 @@ private fun WatchZapsForNote(baseNote: Note, accountViewModel: AccountViewModel, val zapsState by baseNote.live().zaps.observeAsState() LaunchedEffect(key1 = zapsState) { - launch(Dispatchers.Default) { - onWasZapped(accountViewModel.calculateIfNoteWasZappedByAccount(baseNote)) - } + accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, onWasZapped) } } @@ -1171,9 +1128,7 @@ fun WatchZapAmountsForNote(baseNote: Note, accountViewModel: AccountViewModel, o val zapsState by baseNote.live().zaps.observeAsState() LaunchedEffect(key1 = zapsState) { - launch(Dispatchers.Default) { - onZapAmount(showAmount(accountViewModel.calculateZapAmount(baseNote))) - } + accountViewModel.calculateZapAmount(baseNote, onZapAmount) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index c7087e1a7..e35a1fcb0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.ui.note.MultiSetCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable @@ -102,11 +101,9 @@ private fun WatchScrollToTop( val scrollToTop by viewModel.scrollToTop.collectAsState() LaunchedEffect(scrollToTop) { - launch { - if (scrollToTop > 0 && viewModel.scrolltoTopPending) { - listState.scrollToItem(index = 0) - viewModel.sentToTop() - } + if (scrollToTop > 0 && viewModel.scrolltoTopPending) { + listState.scrollToItem(index = 0) + viewModel.sentToTop() } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index d4cb36cf5..7561b818d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -301,11 +301,13 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel(), I fun checkKeysInvalidateDataAndSendToTop() { if (lastFeedKey != localFilter.feedKey()) { - bundler.invalidate(false) { - // adds the time to perform the refresh into this delay - // holding off new updates in case of heavy refresh routines. - refreshSuspended() - sendToTop() + viewModelScope.launch(Dispatchers.IO) { + bundler.invalidate(false) { + // adds the time to perform the refresh into this delay + // holding off new updates in case of heavy refresh routines. + refreshSuspended() + sendToTop() + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index bc6eaab68..eabb999d9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -16,14 +16,22 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountState import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.ConnectivityType +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.amethyst.service.OnlineChecker 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.events.Event import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.LnZapEvent +import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.SealedGossipEvent @@ -121,21 +129,86 @@ class AccountViewModel(val account: Account) : ViewModel() { account.delete(account.boostsTo(note)) } - fun calculateIfNoteWasZappedByAccount(zappedNote: Note): Boolean { - return account.calculateIfNoteWasZappedByAccount(zappedNote) + fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) { + viewModelScope.launch(Dispatchers.Default) { + onWasZapped(account.calculateIfNoteWasZappedByAccount(zappedNote)) + } } - fun calculateZapAmount(zappedNote: Note): BigDecimal { + suspend fun calculateZapAmount(zappedNote: Note): BigDecimal { 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) { viewModelScope.launch(Dispatchers.IO) { 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() if (lud16.isNullOrBlank()) { @@ -388,6 +461,26 @@ class AccountViewModel(val account: Account) : ViewModel() { fun updateStatus(it: AddressableNote, newStatus: String) { 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 { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 00ad2cc63..724c1d574 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -623,7 +623,7 @@ fun ShowVideoStreaming( } url?.let { - CheckIfUrlIsOnline(url) { + CheckIfUrlIsOnline(url, accountViewModel) { Row( verticalAlignment = Alignment.CenterVertically, modifier = remember { Modifier.heightIn(max = 300.dp) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 36de0257a..3b146279b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -29,7 +29,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -136,12 +134,14 @@ private fun HomePages( } @Composable -fun CheckIfUrlIsOnline(url: String, whenOnline: @Composable () -> Unit) { +fun CheckIfUrlIsOnline(url: String, accountViewModel: AccountViewModel, whenOnline: @Composable () -> Unit) { var online by remember { mutableStateOf(false) } LaunchedEffect(key1 = url) { - launch(Dispatchers.IO) { - online = OnlineChecker.isOnline(url) + accountViewModel.checkIfOnline(url) { isOnline -> + if (online != isOnline) { + online = isOnline + } } } @@ -162,11 +162,9 @@ fun WatchAccountForHomeScreen( val followState by accountViewModel.account.userProfile().live().follows.observeAsState() LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList, followState) { - launch(Dispatchers.IO) { - NostrHomeDataSource.invalidateFilters() - homeFeedViewModel.checkKeysInvalidateDataAndSendToTop() - repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop() - } + NostrHomeDataSource.invalidateFilters() + homeFeedViewModel.checkKeysInvalidateDataAndSendToTop() + repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt index d48a598c9..7687ea956 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt @@ -73,42 +73,38 @@ fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, nav fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by baseNote.live().metadata.observeAsState() - val scope = rememberCoroutineScope() - LaunchedEffect(key1 = noteState) { - scope.launch { - val note = noteState?.note ?: return@launch - var event = note.event - val channelHex = note.channelHex() + val note = noteState?.note ?: return@LaunchedEffect + var event = note.event + val channelHex = note.channelHex() - if (event is GiftWrapEvent) { - event = accountViewModel.unwrap(event) - } + if (event is GiftWrapEvent) { + event = accountViewModel.unwrap(event) + } - if (event is SealedGossipEvent) { - event = accountViewModel.unseal(event) - } + if (event is SealedGossipEvent) { + event = accountViewModel.unseal(event) + } - if (event == null) { - // stay here, loading - } else if (event is ChannelCreateEvent) { - nav("Channel/${note.idHex}") - } else if (event is ChatroomKeyable) { - note.author?.let { - val withKey = (event as ChatroomKeyable) - .chatroomKey(accountViewModel.userProfile().pubkeyHex) + if (event == null) { + // stay here, loading + } else if (event is ChannelCreateEvent) { + nav("Channel/${note.idHex}") + } else if (event is ChatroomKeyable) { + note.author?.let { + val withKey = (event as ChatroomKeyable) + .chatroomKey(accountViewModel.userProfile().pubkeyHex) - withContext(Dispatchers.IO) { - accountViewModel.userProfile().createChatroom(withKey) - } - - nav("Room/${withKey.hashCode()}") + withContext(Dispatchers.IO) { + accountViewModel.userProfile().createChatroom(withKey) } - } else if (channelHex != null) { - nav("Channel/$channelHex") - } else { - nav("Note/${note.idHex}") + + nav("Room/${withKey.hashCode()}") } + } else if (channelHex != null) { + nav("Channel/$channelHex") + } else { + nav("Note/${note.idHex}") } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt index 299335c96..9ad3a2c55 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt @@ -50,5 +50,18 @@ class StatusEvent( val sig = CryptoUtils.sign(id, privateKey) 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()) + } } }