mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
refactor(notifications): summary state and feed leftovers off java.*
- NotificationSummaryState buckets by local day through LocalClock and SearchDate (the same yyyy-MM-dd keys DateTimeFormatter produced) and sums with quartz BigDecimal; Double.toBigDecimal() becomes BigDecimal(toString()), which is what it did on the JVM. - showCount moves from ReactionsRow to commons/util. - CardFeedContentState, TopNavFilterState and NotificationSummaryState name themselves with this::class.simpleName, and AccountFeedContentStates asserts off-main through LocalCache.appHost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.routeForUser
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.commons.util.showCount
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@@ -212,6 +212,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.reactionBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ripple24dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.selectedReactionBoxModifier
|
||||
import com.vitorpamplona.amethyst.commons.util.showAmount
|
||||
import com.vitorpamplona.amethyst.commons.util.showCount
|
||||
import com.vitorpamplona.amethyst.model.zap.CashuRailStatus
|
||||
import com.vitorpamplona.amethyst.model.zap.RailCapability
|
||||
import com.vitorpamplona.amethyst.model.zap.RailCapabilityResolver
|
||||
@@ -2899,15 +2900,3 @@ private fun ZapChipPreviewRow(
|
||||
onChangeAmount = {},
|
||||
)
|
||||
}
|
||||
|
||||
fun showCount(count: Int?): String {
|
||||
if (count == null) return ""
|
||||
if (count == 0) return ""
|
||||
|
||||
return when {
|
||||
count >= 1000000000 -> "${(count / 1000000000f).roundToInt()}G"
|
||||
count >= 1000000 -> "${(count / 1000000f).roundToInt()}M"
|
||||
count >= 10000 -> "${(count / 1000f).roundToInt()}k"
|
||||
else -> "$count"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,12 +96,12 @@ import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.amethyst.commons.util.showCount
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.note.showCount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
|
||||
@@ -555,7 +555,7 @@ class TopNavFilterState(
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, teleport, globalFollow, mineFollow, muteListFollow))
|
||||
|
||||
fun destroy() {
|
||||
Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" }
|
||||
Log.d("Init") { "OnCleared: ${this::class.simpleName}" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.dal.ArticlesFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.dal.BadgesFeedFilter
|
||||
@@ -373,7 +372,7 @@ class AccountFeedContentStates(
|
||||
}
|
||||
|
||||
fun updateFeedsWith(newNotes: Set<Note>) {
|
||||
checkNotInMainThread()
|
||||
LocalCache.appHost.assertNotMainThread()
|
||||
|
||||
homeLive.updateFeedWith(newNotes)
|
||||
homeNewThreads.updateFeedWith(newNotes)
|
||||
@@ -447,7 +446,7 @@ class AccountFeedContentStates(
|
||||
}
|
||||
|
||||
fun deleteNotes(newNotes: Set<Note>) {
|
||||
checkNotInMainThread()
|
||||
LocalCache.appHost.assertNotMainThread()
|
||||
|
||||
homeLive.deleteFromFeed(newNotes)
|
||||
homeNewThreads.deleteFromFeed(newNotes)
|
||||
|
||||
+1
-1
@@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.util.showCount
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
@@ -86,7 +87,6 @@ import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.showCount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote
|
||||
|
||||
+5
-5
@@ -501,7 +501,7 @@ class CardFeedContentState(
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
logTime("${this.javaClass.simpleName} Card update") { refreshSuspended() }
|
||||
logTime("${this::class.simpleName} Card update") { refreshSuspended() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ class CardFeedContentState(
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
logTime("${this.javaClass.simpleName} Card update") {
|
||||
logTime("${this::class.simpleName} Card update") {
|
||||
refreshSuspended()
|
||||
sendToTop()
|
||||
}
|
||||
@@ -523,7 +523,7 @@ class CardFeedContentState(
|
||||
bundler.invalidate(false) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
logTime("${this.javaClass.simpleName} Card update: checkKeysInvalidateDataAndSendToTop") {
|
||||
logTime("${this::class.simpleName} Card update: checkKeysInvalidateDataAndSendToTop") {
|
||||
refreshSuspended()
|
||||
sendToTop()
|
||||
}
|
||||
@@ -534,7 +534,7 @@ class CardFeedContentState(
|
||||
fun invalidateInsertData(newItems: Set<Note>) {
|
||||
bundlerInsert.invalidateList(newItems) {
|
||||
val newObjects = it.flattenToSet()
|
||||
logTime("${this.javaClass.simpleName} Card additive receiving ${newObjects.size} items into ${it.size} items") {
|
||||
logTime("${this::class.simpleName} Card additive receiving ${newObjects.size} items into ${it.size} items") {
|
||||
if (newObjects.isNotEmpty()) {
|
||||
refreshFromOldState(newObjects)
|
||||
}
|
||||
@@ -559,7 +559,7 @@ class CardFeedContentState(
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" }
|
||||
Log.d("Init") { "OnCleared: ${this::class.simpleName}" }
|
||||
clear()
|
||||
bundlerInsert.cancel()
|
||||
bundler.cancel()
|
||||
|
||||
+24
-28
@@ -29,10 +29,11 @@ import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock
|
||||
import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate
|
||||
import com.vitorpamplona.amethyst.commons.service.BundledInsert
|
||||
import com.vitorpamplona.amethyst.commons.util.showAmountInteger
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.showCount
|
||||
import com.vitorpamplona.amethyst.commons.util.showCount
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
@@ -42,17 +43,15 @@ import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
|
||||
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent
|
||||
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent
|
||||
import com.vitorpamplona.quartz.utils.BigDecimal
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.plus
|
||||
import com.vitorpamplona.quartz.utils.toDoubleValue
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
val ShowDecimals = ExtraStore.Key<Boolean>()
|
||||
val BottomAxisLabelKey = ExtraStore.Key<List<String>>()
|
||||
@@ -72,22 +71,19 @@ class NotificationSummaryState(
|
||||
val chartModel = _chartModel.asStateFlow()
|
||||
|
||||
private var takenIntoAccount = setOf<HexKey>()
|
||||
private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
|
||||
|
||||
val todaysReplyCount = replies.map { showCount(it[today()]) }.distinctUntilChanged()
|
||||
val todaysBoostCount = boosts.map { showCount(it[today()]) }.distinctUntilChanged()
|
||||
val todaysReactionCount = reactions.map { showCount(it[today()]) }.distinctUntilChanged()
|
||||
val todaysZapAmount = zaps.map { showAmountInteger(it[today()]) }.distinctUntilChanged()
|
||||
|
||||
fun formatDate(createAt: Long): String =
|
||||
sdf.format(
|
||||
Instant.ofEpochSecond(createAt).atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
||||
)
|
||||
// Buckets are local calendar days, keyed yyyy-MM-dd.
|
||||
fun formatDate(createAt: Long): String = SearchDate.civilFromDays(LocalClock.epochDayCounter().epochDay(createAt)).ymd()
|
||||
|
||||
fun today(): String = sdf.format(LocalDateTime.now())
|
||||
fun today(): String = LocalClock.today().ymd()
|
||||
|
||||
suspend fun initializeSuspend() {
|
||||
checkNotInMainThread()
|
||||
LocalCache.appHost.assertNotMainThread()
|
||||
|
||||
val currentUser = user.pubkeyHex
|
||||
|
||||
@@ -121,7 +117,7 @@ class NotificationSummaryState(
|
||||
// the user might be sending his own receipts noteEvent.pubKey != currentUser
|
||||
if (noteEvent.isTaggedUser(currentUser)) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal(0)) + (noteEvent.amount ?: BigDecimal(0))
|
||||
takenIntoAccount.add(noteEvent.id)
|
||||
}
|
||||
}
|
||||
@@ -131,7 +127,7 @@ class NotificationSummaryState(
|
||||
val amount = noteEvent.claimedAmountInSats()
|
||||
if (amount != null) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + BigDecimal.valueOf(amount)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal(0)) + BigDecimal(amount)
|
||||
takenIntoAccount.add(noteEvent.id)
|
||||
}
|
||||
}
|
||||
@@ -142,7 +138,7 @@ class NotificationSummaryState(
|
||||
val amount = noteEvent.amount()
|
||||
if (amount != null) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + BigDecimal.valueOf(amount / 1000)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal(0)) + BigDecimal(amount / 1000)
|
||||
takenIntoAccount.add(noteEvent.id)
|
||||
}
|
||||
}
|
||||
@@ -178,7 +174,7 @@ class NotificationSummaryState(
|
||||
}
|
||||
|
||||
suspend fun addToStatsSuspend(newBlockNotes: Set<Set<Note>>) {
|
||||
checkNotInMainThread()
|
||||
LocalCache.appHost.assertNotMainThread()
|
||||
|
||||
val currentUser = user.pubkeyHex
|
||||
|
||||
@@ -216,7 +212,7 @@ class NotificationSummaryState(
|
||||
if (noteEvent.isTaggedUser(currentUser)) {
|
||||
// && noteEvent.pubKey != currentUser User might be sending his own receipts
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal(0)) + (noteEvent.amount ?: BigDecimal(0))
|
||||
takenIntoAccount.add(noteEvent.id)
|
||||
hasNewElements = true
|
||||
}
|
||||
@@ -227,7 +223,7 @@ class NotificationSummaryState(
|
||||
val amount = noteEvent.claimedAmountInSats()
|
||||
if (amount != null) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + BigDecimal.valueOf(amount)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal(0)) + BigDecimal(amount)
|
||||
takenIntoAccount.add(noteEvent.id)
|
||||
hasNewElements = true
|
||||
}
|
||||
@@ -239,7 +235,7 @@ class NotificationSummaryState(
|
||||
val amount = noteEvent.amount()
|
||||
if (amount != null) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + BigDecimal.valueOf(amount / 1000)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal(0)) + BigDecimal(amount / 1000)
|
||||
takenIntoAccount.add(noteEvent.id)
|
||||
hasNewElements = true
|
||||
}
|
||||
@@ -280,12 +276,12 @@ class NotificationSummaryState(
|
||||
}
|
||||
|
||||
private suspend fun refreshChartModel() {
|
||||
checkNotInMainThread()
|
||||
LocalCache.appHost.assertNotMainThread()
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
val today = LocalClock.today()
|
||||
|
||||
val dataAxisLabelIndexes = listOf(-6, -5, -4, -3, -2, -1, 0)
|
||||
val dataAxisLabels = dataAxisLabelIndexes.map { sdf.format(now.plusDays(it.toLong())) }
|
||||
val dataAxisLabels = dataAxisLabelIndexes.map { today.plusDays(it).ymd() }
|
||||
|
||||
val chart1 =
|
||||
LineCartesianLayerModel.build {
|
||||
@@ -296,7 +292,7 @@ class NotificationSummaryState(
|
||||
|
||||
val chart2 =
|
||||
LineCartesianLayerModel.build {
|
||||
series(dataAxisLabelIndexes, dataAxisLabels.map { zaps.value[it]?.toFloat() ?: 0f })
|
||||
series(dataAxisLabelIndexes, dataAxisLabels.map { zaps.value[it]?.toDoubleValue()?.toFloat() ?: 0f })
|
||||
}
|
||||
|
||||
val model = CartesianChartModel(chart1, chart2)
|
||||
@@ -315,9 +311,9 @@ class NotificationSummaryState(
|
||||
): Boolean {
|
||||
val step = (max - min) / 8
|
||||
|
||||
var previous = showAmountInteger(min.toBigDecimal())
|
||||
var previous = showAmountInteger(BigDecimal(min.toString()))
|
||||
for (i in 1..7) {
|
||||
val current = showAmountInteger((min + (i * step)).toBigDecimal())
|
||||
val current = showAmountInteger(BigDecimal((min + (i * step)).toString()))
|
||||
if (previous == current) {
|
||||
return true
|
||||
}
|
||||
@@ -335,6 +331,6 @@ class NotificationSummaryState(
|
||||
|
||||
fun destroy() {
|
||||
bundlerInsert.cancel()
|
||||
Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" }
|
||||
Log.d("Init") { "OnCleared: ${this::class.simpleName}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.util
|
||||
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/** A count in compact form (12k, 3M), or empty for none. */
|
||||
fun showCount(count: Int?): String {
|
||||
if (count == null) return ""
|
||||
if (count == 0) return ""
|
||||
|
||||
return when {
|
||||
count >= 1000000000 -> "${(count / 1000000000f).roundToInt()}G"
|
||||
count >= 1000000 -> "${(count / 1000000f).roundToInt()}M"
|
||||
count >= 10000 -> "${(count / 1000f).roundToInt()}k"
|
||||
else -> "$count"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user