From decb146736e5f6d4f6e03027e00b9f9ea5e53cb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 14:35:11 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../vitorpamplona/amethyst/ui/note/Gallery.kt | 1 + .../amethyst/ui/note/ReactionsRow.kt | 13 +---- .../amethyst/ui/note/types/Poll.kt | 2 +- .../amethyst/ui/screen/TopNavFilterState.kt | 2 +- .../loggedIn/AccountFeedContentStates.kt | 5 +- .../send/PrivateMessageEditFieldRow.kt | 2 +- .../notifications/CardFeedContentState.kt | 10 ++-- .../notifications/NotificationSummaryState.kt | 52 +++++++++---------- .../amethyst/commons/util/CountFormat.kt | 36 +++++++++++++ 9 files changed, 72 insertions(+), 51 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/CountFormat.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt index b60440f8c1..4dbde3bcc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 9bbd61b21b..c1691b7b8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -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" - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index a0c92badb1..f099a3714c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 6e8f0f81ef..248c1e2abf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -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}" } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 6745913934..269c35a02a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -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) { - checkNotInMainThread() + LocalCache.appHost.assertNotMainThread() homeLive.updateFeedWith(newNotes) homeNewThreads.updateFeedWith(newNotes) @@ -447,7 +446,7 @@ class AccountFeedContentStates( } fun deleteNotes(newNotes: Set) { - checkNotInMainThread() + LocalCache.appHost.assertNotMainThread() homeLive.deleteFromFeed(newNotes) homeNewThreads.deleteFromFeed(newNotes) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 11b011c810..065ed77a2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index 662b2a46bb..516dbe0659 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -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) { 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() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index f6bdbccbbf..f70ddc3b7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -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() val BottomAxisLabelKey = ExtraStore.Key>() @@ -72,22 +71,19 @@ class NotificationSummaryState( val chartModel = _chartModel.asStateFlow() private var takenIntoAccount = setOf() - 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>) { - 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}" } } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/CountFormat.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/CountFormat.kt new file mode 100644 index 0000000000..41475c690a --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/CountFormat.kt @@ -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" + } +}