Moves to vico chart 2.0

This commit is contained in:
Vitor Pamplona
2025-02-14 11:05:00 -05:00
parent dcb0e31cd6
commit 0f6c3b9a3a
15 changed files with 419 additions and 263 deletions
@@ -42,7 +42,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ThreadAssemblerTest { class ThreadDualAxisChartAssemblerTest {
val db = val db =
""" """
[ [
@@ -86,7 +86,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding
@@ -386,7 +385,7 @@ fun InnerRenderClassifiedsThumb(
card.price?.let { card.price?.let {
val priceTag = val priceTag =
remember(card) { remember(card) {
val newAmount = it.amount.toBigDecimalOrNull()?.let { showAmountAxis(it) } ?: it.amount val newAmount = it.amount.toBigDecimalOrNull()?.let { showAmountInteger(it) } ?: it.amount
if (it.frequency != null && it.currency != null) { if (it.frequency != null && it.currency != null) {
"$newAmount ${it.currency}/${it.frequency}" "$newAmount ${it.currency}/${it.frequency}"
@@ -74,3 +74,10 @@ fun showAmount(amount: BigDecimal?): String {
else -> dfN.get().format(amount) else -> dfN.get().format(amount)
} }
} }
fun showAmountWithZero(amount: BigDecimal?): String {
if (amount == null) return "0"
if (amount.abs() < BigDecimal(0.01)) return "0"
return showAmount(amount)
}
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2024 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.ui.note
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
private val dfG =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#G")
}
private val dfM =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#M")
}
private val dfK =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#k")
}
private val dfN =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#")
}
fun showAmountInteger(amount: BigDecimal?): String {
if (amount == null) return ""
if (amount.abs() < BigDecimal(0.01)) return ""
return when {
amount >= OneGiga -> dfG.get().format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP))
amount >= OneMega -> dfM.get().format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP))
amount >= TenKilo -> dfK.get().format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP))
else -> dfN.get().format(amount)
}
}
fun showAmountIntegerWithZero(amount: BigDecimal?): String {
if (amount == null) return "0"
if (amount.abs() < BigDecimal(0.01)) return "0"
return showAmountInteger(amount)
}
@@ -47,7 +47,6 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ShowUserButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ShowUserButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton
@@ -144,7 +143,7 @@ private fun ZapAmount(zapEventNote: Note) {
LaunchedEffect(key1 = noteState) { LaunchedEffect(key1 = noteState) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val newZapAmount = showAmountAxis((noteState?.note?.event as? LnZapEvent)?.amount) val newZapAmount = showAmountInteger((noteState?.note?.event as? LnZapEvent)?.amount)
if (zapAmount != newZapAmount) { if (zapAmount != newZapAmount) {
zapAmount = newZapAmount zapAmount = newZapAmount
} }
@@ -64,11 +64,11 @@ import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
import com.vitorpamplona.amethyst.ui.screen.SettingsState import com.vitorpamplona.amethyst.ui.screen.SettingsState
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.ammolite.relays.BundledInsert
@@ -536,7 +536,7 @@ class AccountViewModel(
it.request.event it.request.event
?.content ?.content
?.ifBlank { null }, ?.ifBlank { null },
showAmountAxis((it.response.event as? LnZapEvent)?.amount), showAmountInteger((it.response.event as? LnZapEvent)?.amount),
) )
}.toMutableMap() }.toMutableMap()
@@ -569,7 +569,7 @@ class AccountViewModel(
ZapAmountCommentNotification( ZapAmountCommentNotification(
LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.request.author, LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.request.author,
cachedPrivateRequest.content.ifBlank { null }, cachedPrivateRequest.content.ifBlank { null },
showAmountAxis((it.response.event as? LnZapEvent)?.amount), showAmountInteger((it.response.event as? LnZapEvent)?.amount),
) )
} else { } else {
ZapAmountCommentNotification( ZapAmountCommentNotification(
@@ -577,7 +577,7 @@ class AccountViewModel(
it.request.event it.request.event
?.content ?.content
?.ifBlank { null }, ?.ifBlank { null },
showAmountAxis((it.response.event as? LnZapEvent)?.amount), showAmountInteger((it.response.event as? LnZapEvent)?.amount),
) )
} }
} else { } else {
@@ -586,7 +586,7 @@ class AccountViewModel(
it.request.event it.request.event
?.content ?.content
?.ifBlank { null }, ?.ifBlank { null },
showAmountAxis((it.response.event as? LnZapEvent)?.amount), showAmountInteger((it.response.event as? LnZapEvent)?.amount),
) )
} }
}.toImmutableList() }.toImmutableList()
@@ -603,7 +603,7 @@ class AccountViewModel(
ZapAmountCommentNotification( ZapAmountCommentNotification(
LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.first.author, LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.first.author,
cachedPrivateRequest.content.ifBlank { null }, cachedPrivateRequest.content.ifBlank { null },
showAmountAxis((it.second?.event as? LnZapEvent)?.amount), showAmountInteger((it.second?.event as? LnZapEvent)?.amount),
) )
} else { } else {
ZapAmountCommentNotification( ZapAmountCommentNotification(
@@ -611,7 +611,7 @@ class AccountViewModel(
it.first.event it.first.event
?.content ?.content
?.ifBlank { null }, ?.ifBlank { null },
showAmountAxis((it.second?.event as? LnZapEvent)?.amount), showAmountInteger((it.second?.event as? LnZapEvent)?.amount),
) )
} }
} else { } else {
@@ -620,7 +620,7 @@ class AccountViewModel(
it.first.event it.first.event
?.content ?.content
?.ifBlank { null }, ?.ifBlank { null },
showAmountAxis((it.second?.event as? LnZapEvent)?.amount), showAmountInteger((it.second?.event as? LnZapEvent)?.amount),
) )
} }
}.toImmutableList() }.toImmutableList()
@@ -642,7 +642,7 @@ class AccountViewModel(
it.first.event it.first.event
?.content ?.content
?.ifBlank { null }, ?.ifBlank { null },
showAmountAxis((it.second?.event as? LnZapEvent)?.amount), showAmountInteger((it.second?.event as? LnZapEvent)?.amount),
) )
}.toMutableMap() }.toMutableMap()
@@ -687,7 +687,7 @@ class AccountViewModel(
ZapAmountCommentNotification( ZapAmountCommentNotification(
newAuthor, newAuthor,
decryptedContent.content.ifBlank { null }, decryptedContent.content.ifBlank { null },
showAmountAxis(amount), showAmountInteger(amount),
), ),
) )
} }
@@ -698,7 +698,7 @@ class AccountViewModel(
ZapAmountCommentNotification( ZapAmountCommentNotification(
zapRequest.author, zapRequest.author,
zapRequest.event?.content?.ifBlank { null }, zapRequest.event?.content?.ifBlank { null },
showAmountAxis(amount), showAmountInteger(amount),
), ),
) )
} }
@@ -22,16 +22,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
import android.util.Log import android.util.Log
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import com.patrykandpatrick.vico.core.common.data.ExtraStore
import com.patrykandpatrick.vico.core.entry.composed.plus import com.patrykandpatrick.vico.core.common.data.MutableExtraStore
import com.patrykandpatrick.vico.core.entry.entryOf
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache 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.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
import com.vitorpamplona.amethyst.ui.note.showCount import com.vitorpamplona.amethyst.ui.note.showCount
import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.ammolite.relays.BundledInsert
import com.vitorpamplona.quartz.nip01Core.HexKey import com.vitorpamplona.quartz.nip01Core.HexKey
@@ -52,37 +52,30 @@ import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
val ShowDecimals = ExtraStore.Key<Boolean>()
val BottomAxisLabelKey = ExtraStore.Key<List<String>>()
@Stable @Stable
class NotificationSummaryState( class NotificationSummaryState(
val account: Account, val account: Account,
) { ) {
val user: User = account.userProfile() val user: User = account.userProfile()
private var _reactions = MutableStateFlow<Map<String, Int>>(emptyMap()) private var reactions = MutableStateFlow<Map<String, Int>>(emptyMap())
private var _boosts = MutableStateFlow<Map<String, Int>>(emptyMap()) private var boosts = MutableStateFlow<Map<String, Int>>(emptyMap())
private var _zaps = MutableStateFlow<Map<String, BigDecimal>>(emptyMap()) private var zaps = MutableStateFlow<Map<String, BigDecimal>>(emptyMap())
private var _replies = MutableStateFlow<Map<String, Int>>(emptyMap()) private var replies = MutableStateFlow<Map<String, Int>>(emptyMap())
private var _chartModel = MutableStateFlow<ComposedChartEntryModel<ChartEntryModel>?>(null)
private var _axisLabels = MutableStateFlow<List<String>>(emptyList())
val reactions = _reactions.asStateFlow()
val boosts = _boosts.asStateFlow()
val zaps = _zaps.asStateFlow()
val replies = _replies.asStateFlow()
private var _chartModel = MutableStateFlow<CartesianChartModel?>(null)
val chartModel = _chartModel.asStateFlow() val chartModel = _chartModel.asStateFlow()
val axisLabels = _axisLabels.asStateFlow()
private var takenIntoAccount = setOf<HexKey>() private var takenIntoAccount = setOf<HexKey>()
private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat() private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
val todaysReplyCount = _replies.map { showCount(it[today()]) }.distinctUntilChanged() val todaysReplyCount = replies.map { showCount(it[today()]) }.distinctUntilChanged()
val todaysBoostCount = _boosts.map { showCount(it[today()]) }.distinctUntilChanged() val todaysBoostCount = boosts.map { showCount(it[today()]) }.distinctUntilChanged()
val todaysReactionCount = _reactions.map { showCount(it[today()]) }.distinctUntilChanged() val todaysReactionCount = reactions.map { showCount(it[today()]) }.distinctUntilChanged()
val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) }.distinctUntilChanged() val todaysZapAmount = zaps.map { showAmountInteger(it[today()]) }.distinctUntilChanged()
var shouldShowDecimalsInAxis = false
fun formatDate(createAt: Long): String = fun formatDate(createAt: Long): String =
sdf.format( sdf.format(
@@ -146,10 +139,10 @@ class NotificationSummaryState(
} }
this.takenIntoAccount = takenIntoAccount this.takenIntoAccount = takenIntoAccount
this._reactions.emit(reactions) this.reactions.emit(reactions)
this._replies.emit(replies) this.replies.emit(replies)
this._zaps.emit(zaps) this.zaps.emit(zaps)
this._boosts.emit(boosts) this.boosts.emit(boosts)
refreshChartModel() refreshChartModel()
} }
@@ -159,10 +152,10 @@ class NotificationSummaryState(
val currentUser = user.pubkeyHex val currentUser = user.pubkeyHex
val reactions = this._reactions.value.toMutableMap() val reactions = this.reactions.value.toMutableMap()
val boosts = this._boosts.value.toMutableMap() val boosts = this.boosts.value.toMutableMap()
val zaps = this._zaps.value.toMutableMap() val zaps = this.zaps.value.toMutableMap()
val replies = this._replies.value.toMutableMap() val replies = this.replies.value.toMutableMap()
val takenIntoAccount = this.takenIntoAccount.toMutableSet() val takenIntoAccount = this.takenIntoAccount.toMutableSet()
var hasNewElements = false var hasNewElements = false
@@ -217,10 +210,10 @@ class NotificationSummaryState(
if (hasNewElements) { if (hasNewElements) {
this.takenIntoAccount = takenIntoAccount this.takenIntoAccount = takenIntoAccount
this._reactions.emit(reactions) this.reactions.emit(reactions)
this._replies.emit(replies) this.replies.emit(replies)
this._zaps.emit(zaps) this.zaps.emit(zaps)
this._boosts.emit(boosts) this.boosts.emit(boosts)
refreshChartModel() refreshChartModel()
} }
@@ -229,59 +222,42 @@ class NotificationSummaryState(
private suspend fun refreshChartModel() { private suspend fun refreshChartModel() {
checkNotInMainThread() checkNotInMainThread()
val day = 24 * 60 * 60L
val now = LocalDateTime.now() val now = LocalDateTime.now()
val displayAxisFormatter = DateTimeFormatter.ofPattern("EEE")
val dataAxisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { sdf.format(now.minusSeconds(day * it)) } val dataAxisLabelIndexes = listOf(-6, -5, -4, -3, -2, -1, 0)
val dataAxisLabels = dataAxisLabelIndexes.map { sdf.format(now.plusDays(it.toLong())) }
val listOfCountCurves = val chart1 =
listOf( LineCartesianLayerModel.build {
dataAxisLabels.mapIndexed { index, dateStr -> series(dataAxisLabelIndexes, dataAxisLabels.map { replies.value[it]?.toFloat() ?: 0f })
entryOf(index, _replies.value[dateStr]?.toFloat() ?: 0f) series(dataAxisLabelIndexes, dataAxisLabels.map { boosts.value[it]?.toFloat() ?: 0f })
}, series(dataAxisLabelIndexes, dataAxisLabels.map { reactions.value[it]?.toFloat() ?: 0f })
dataAxisLabels.mapIndexed { index, dateStr ->
entryOf(index, _boosts.value[dateStr]?.toFloat() ?: 0f)
},
dataAxisLabels.mapIndexed { index, dateStr ->
entryOf(index, _reactions.value[dateStr]?.toFloat() ?: 0f)
},
)
val listOfValueCurves =
listOf(
dataAxisLabels.mapIndexed { index, dateStr ->
entryOf(index, _zaps.value[dateStr]?.toFloat() ?: 0f)
},
)
val chartEntryModelProducer1 = ChartEntryModelProducer(listOfCountCurves).getModel()
val chartEntryModelProducer2 = ChartEntryModelProducer(listOfValueCurves).getModel()
chartEntryModelProducer1?.let { chart1 ->
chartEntryModelProducer2?.let { chart2 ->
this.shouldShowDecimalsInAxis = shouldShowDecimals(chart2.minY, chart2.maxY)
this._axisLabels.emit(
listOf(6, 5, 4, 3, 2, 1, 0).map {
displayAxisFormatter.format(now.minusSeconds(day * it))
},
)
this._chartModel.emit(chart1.plus(chart2))
} }
}
val chart2 =
LineCartesianLayerModel.build {
series(dataAxisLabelIndexes, dataAxisLabels.map { zaps.value[it]?.toFloat() ?: 0f })
}
val model = CartesianChartModel(chart1, chart2)
val mutableStore = MutableExtraStore()
mutableStore[ShowDecimals] = shouldShowDecimals(chart2.minY, chart2.maxY)
this._chartModel.emit(model.copy(mutableStore))
} }
// determine if the min max are so close that they render to the same number. // determine if the min max are so close that they render to the same number.
fun shouldShowDecimals( fun shouldShowDecimals(
min: Float, min: Double,
max: Float, max: Double,
): Boolean { ): Boolean {
val step = (max - min) / 8 val step = (max - min) / 8
var previous = showAmountAxis(min.toBigDecimal()) var previous = showAmountInteger(min.toBigDecimal())
for (i in 1..7) { for (i in 1..7) {
val current = showAmountAxis((min + (i * step)).toBigDecimal()) val current = showAmountInteger((min + (i * step)).toBigDecimal())
if (previous == current) { if (previous == current) {
return true return true
} }
@@ -30,47 +30,17 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.patrykandpatrick.vico.compose.axis.axisLabelComponent import com.patrykandpatrick.vico.compose.common.ProvideVicoTheme
import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.rememberEndAxis
import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.compose.component.shape.shader.fromBrush
import com.patrykandpatrick.vico.compose.style.ProvideChartStyle
import com.patrykandpatrick.vico.core.DefaultAlpha
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.chart.composed.plus
import com.patrykandpatrick.vico.core.chart.line.LineChart
import com.patrykandpatrick.vico.core.chart.values.ChartValues
import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders
import com.vitorpamplona.amethyst.ui.note.OneGiga
import com.vitorpamplona.amethyst.ui.note.OneKilo
import com.vitorpamplona.amethyst.ui.note.OneMega
import com.vitorpamplona.amethyst.ui.note.TenKilo
import com.vitorpamplona.amethyst.ui.note.UserReactionsRow import com.vitorpamplona.amethyst.ui.note.UserReactionsRow
import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart.ShowChart
import com.vitorpamplona.amethyst.ui.note.showCount
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
import com.vitorpamplona.amethyst.ui.theme.chartStyle import com.vitorpamplona.amethyst.ui.theme.chartStyle
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.math.roundToInt
@Composable @Composable
fun SummaryBar(state: NotificationSummaryState) { fun SummaryBar(state: NotificationSummaryState) {
@@ -83,137 +53,24 @@ fun SummaryBar(state: NotificationSummaryState) {
enter = slideInVertically() + expandVertically(), enter = slideInVertically() + expandVertically(),
exit = slideOutVertically() + shrinkVertically(), exit = slideOutVertically() + shrinkVertically(),
) { ) {
val lineChartCount =
lineChart(
lines =
listOf(RoyalBlue, Color.Green, Color.Red).map { lineChartColor ->
LineChart.LineSpec(
lineColor = lineChartColor.toArgb(),
lineBackgroundShader =
DynamicShaders.fromBrush(
Brush.verticalGradient(
listOf(
lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START),
lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END),
),
),
),
)
},
targetVerticalAxisPosition = AxisPosition.Vertical.Start,
)
val lineChartZaps =
lineChart(
lines =
listOf(BitcoinOrange).map { lineChartColor ->
LineChart.LineSpec(
lineColor = lineChartColor.toArgb(),
lineBackgroundShader =
DynamicShaders.fromBrush(
Brush.verticalGradient(
listOf(
lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START),
lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END),
),
),
),
)
},
targetVerticalAxisPosition = AxisPosition.Vertical.End,
)
Row( Row(
modifier = modifier =
Modifier Modifier
.padding(vertical = 0.dp, horizontal = 20.dp) .padding(vertical = 0.dp, horizontal = 20.dp)
.clickable(onClick = { showChart = !showChart }), .clickable(onClick = { showChart = !showChart }),
) { ) {
ProvideChartStyle( ProvideVicoTheme(MaterialTheme.colorScheme.chartStyle) {
chartStyle = MaterialTheme.colorScheme.chartStyle, ObserveAndShowChart(state)
) {
ObserveAndShowChart(state, lineChartCount, lineChartZaps)
} }
} }
} }
} }
@Composable @Composable
private fun ObserveAndShowChart( private fun ObserveAndShowChart(state: NotificationSummaryState) {
state: NotificationSummaryState,
lineChartCount: LineChart,
lineChartZaps: LineChart,
) {
val axisModel = state.axisLabels.collectAsStateWithLifecycle()
val chartModel by state.chartModel.collectAsStateWithLifecycle() val chartModel by state.chartModel.collectAsStateWithLifecycle()
chartModel?.let { chartModel?.let {
Chart( ShowChart(it)
chart = remember(lineChartCount, lineChartZaps) { lineChartCount.plus(lineChartZaps) },
model = it,
startAxis =
rememberStartAxis(
valueFormatter = CountAxisValueFormatter(),
),
endAxis =
rememberEndAxis(
label = axisLabelComponent(color = BitcoinOrange),
valueFormatter = AmountAxisValueFormatter(state.shouldShowDecimalsInAxis),
),
bottomAxis =
rememberBottomAxis(
valueFormatter = LabelValueFormatter(axisModel),
),
)
}
}
@Stable
class LabelValueFormatter(
val axisLabels: State<List<String>>,
) : AxisValueFormatter<AxisPosition.Horizontal.Bottom> {
override fun formatValue(
value: Float,
chartValues: ChartValues,
): String = axisLabels.value[value.roundToInt()]
}
@Stable
class CountAxisValueFormatter : AxisValueFormatter<AxisPosition.Vertical.Start> {
override fun formatValue(
value: Float,
chartValues: ChartValues,
): String = showCount(value.roundToInt())
}
@Stable
class AmountAxisValueFormatter(
val showDecimals: Boolean,
) : AxisValueFormatter<AxisPosition.Vertical.End> {
override fun formatValue(
value: Float,
chartValues: ChartValues,
): String =
if (showDecimals) {
showAmount(value.toBigDecimal())
} else {
showAmountAxis(value.toBigDecimal())
}
}
var dfG: DecimalFormat = DecimalFormat("#G")
var dfM: DecimalFormat = DecimalFormat("#M")
var dfK: DecimalFormat = DecimalFormat("#k")
var dfN: DecimalFormat = DecimalFormat("#")
fun showAmountAxis(amount: BigDecimal?): String {
if (amount == null) return ""
if (amount.abs() < BigDecimal(0.01)) return ""
return when {
amount >= OneGiga -> dfG.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP))
amount >= OneMega -> dfM.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP))
amount >= TenKilo -> dfK.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP))
else -> dfN.format(amount)
} }
} }
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2024 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.ui.screen.loggedIn.notifications.chart
import androidx.compose.runtime.Stable
import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext
import com.patrykandpatrick.vico.core.cartesian.axis.Axis
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.vitorpamplona.amethyst.ui.note.showAmountIntegerWithZero
import com.vitorpamplona.amethyst.ui.note.showAmountWithZero
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.ShowDecimals
@Stable
class AmountValueFormatter : CartesianValueFormatter {
override fun format(
context: CartesianMeasuringContext,
value: Double,
verticalAxisPosition: Axis.Position.Vertical?,
): CharSequence =
if (context.model.extraStore[ShowDecimals]) {
showAmountWithZero(value.toBigDecimal())
} else {
showAmountIntegerWithZero(value.toBigDecimal())
}
}
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2024 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.ui.screen.loggedIn.notifications.chart
import androidx.compose.runtime.Stable
import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext
import com.patrykandpatrick.vico.core.cartesian.axis.Axis
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import kotlin.math.roundToInt
@Stable
class CountAxisValueFormatter : CartesianValueFormatter {
private fun showCountChart(count: Int?): String {
if (count == null) return "0"
return when {
count >= 1000000000 -> "${(count / 1000000000f).roundToInt()}G"
count >= 1000000 -> "${(count / 1000000f).roundToInt()}M"
count >= 10000 -> "${(count / 1000f).roundToInt()}k"
else -> "$count"
}
}
override fun format(
context: CartesianMeasuringContext,
value: Double,
verticalAxisPosition: Axis.Position.Vertical?,
): CharSequence = showCountChart(value.roundToInt())
}
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2024 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.ui.screen.loggedIn.notifications.chart
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext
import com.patrykandpatrick.vico.core.cartesian.axis.Axis
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt
@Stable
class LastWeekLabelFormatter : CartesianValueFormatter {
private val displayAxisFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("EEE")
private val now = LocalDateTime.now()
private val cache = LruCache<Int, String>(10)
override fun format(
context: CartesianMeasuringContext,
value: Double,
verticalAxisPosition: Axis.Position.Vertical?,
): CharSequence {
val key = value.roundToInt()
cache[key]?.let { return it }
val text = displayAxisFormatter.format(now.plusDays(key.toLong()))
cache.put(key, text)
return text
}
}
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2024 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.ui.screen.loggedIn.notifications.chart
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottom
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberEnd
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStart
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
import com.patrykandpatrick.vico.compose.common.fill
import com.patrykandpatrick.vico.core.cartesian.axis.Axis
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
import com.patrykandpatrick.vico.core.common.shader.ShaderProvider
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
fun makeLine(color: Color): LineCartesianLayer.Line =
LineCartesianLayer.Line(
fill = LineCartesianLayer.LineFill.single(fill(color)),
areaFill =
LineCartesianLayer.AreaFill.single(
fill(
ShaderProvider.verticalGradient(
color.copy(alpha = 0.4f).toArgb(),
Color.Transparent.toArgb(),
),
),
),
pointConnector = LineCartesianLayer.PointConnector.cubic(),
)
val chartLayers =
arrayOf(
LineCartesianLayer(
LineCartesianLayer.LineProvider.series(
makeLine(RoyalBlue),
makeLine(Color.Green),
makeLine(Color.Red),
),
verticalAxisPosition = Axis.Position.Vertical.Start,
),
LineCartesianLayer(
LineCartesianLayer.LineProvider.series(
makeLine(BitcoinOrange),
),
verticalAxisPosition = Axis.Position.Vertical.End,
),
)
@Composable
fun ShowChart(model: CartesianChartModel) {
val chart =
rememberCartesianChart(
layers = chartLayers,
startAxis =
VerticalAxis.rememberStart(
valueFormatter = CountAxisValueFormatter(),
itemPlacer = VerticalAxis.ItemPlacer.count({ 7 }),
),
endAxis =
VerticalAxis.rememberEnd(
label = rememberAxisLabelComponent(color = BitcoinOrange),
valueFormatter = AmountValueFormatter(),
itemPlacer = VerticalAxis.ItemPlacer.count({ 7 }),
),
bottomAxis =
HorizontalAxis.rememberBottom(
valueFormatter = LastWeekLabelFormatter(),
),
)
CartesianChartHost(
chart = chart,
model = model,
)
}
@@ -145,6 +145,7 @@ import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.externalLinkForUser import com.vitorpamplona.amethyst.ui.note.externalLinkForUser
import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel
@@ -159,7 +160,6 @@ import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.RenderGalleryFeed import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.RenderGalleryFeed
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRDialog
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@@ -729,7 +729,7 @@ private fun ZapTabHeader(baseUser: User) {
} }
} }
Text(text = "${showAmountAxis(zapAmount)} ${stringRes(id = R.string.zaps)}") Text(text = "${showAmountInteger(zapAmount)} ${stringRes(id = R.string.zaps)}")
} }
@Composable @Composable
@@ -58,8 +58,8 @@ import androidx.core.view.WindowCompat
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.resolveDefaults import com.halilibo.richtext.ui.resolveDefaults
import com.patrykandpatrick.vico.compose.style.ChartStyle import com.patrykandpatrick.vico.compose.common.VicoTheme
import com.patrykandpatrick.vico.core.DefaultColors import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors
import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.model.ThemeType
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
@@ -441,22 +441,34 @@ val ColorScheme.largeRelayIconModifier: Modifier
val ColorScheme.selectedReactionBoxModifier: Modifier val ColorScheme.selectedReactionBoxModifier: Modifier
get() = if (isLight) LightSelectedReactionBoxModifier else DarkSelectedReactionBoxModifier get() = if (isLight) LightSelectedReactionBoxModifier else DarkSelectedReactionBoxModifier
val ColorScheme.chartStyle: ChartStyle val chartLightColors =
get() { VicoTheme(
val defaultColors = if (isLight) DefaultColors.Light else DefaultColors.Dark candlestickCartesianLayerColors =
return ChartStyle.fromColors( CandlestickCartesianLayerColors(
axisLabelColor = Color(defaultColors.axisLabelColor), Color(0xff0ac285),
axisGuidelineColor = Color(defaultColors.axisGuidelineColor), Color(0xff000000),
axisLineColor = Color(defaultColors.axisLineColor), Color(0xffe8304f),
entityColors = ),
listOf( columnCartesianLayerColors = listOf(Color(0xff3287ff), Color(0xff0ac285), Color(0xffffab02)),
defaultColors.entity1Color, lineColor = Color(0xffbcbfc2),
defaultColors.entity2Color, textColor = Color(0xff000000),
defaultColors.entity3Color, )
).map(::Color),
elevationOverlayColor = Color(defaultColors.elevationOverlayColor), val chartDarkColors =
) VicoTheme(
} candlestickCartesianLayerColors =
CandlestickCartesianLayerColors(
Color(0xff0ac285),
Color(0xffffffff),
Color(0xffe8304f),
),
columnCartesianLayerColors = listOf(Color(0xff3287ff), Color(0xff0ac285), Color(0xffffab02)),
lineColor = Color(0xff494c50),
textColor = Color(0xffffffff),
)
val ColorScheme.chartStyle: VicoTheme
get() = if (isLight) chartLightColors else chartDarkColors
@Composable @Composable
fun AmethystTheme( fun AmethystTheme(
+1 -1
View File
@@ -46,7 +46,7 @@ torAndroid = "0.4.8.12"
translate = "17.0.3" translate = "17.0.3"
unifiedpush = "2.3.1" unifiedpush = "2.3.1"
urlDetector = "0.1.23" urlDetector = "0.1.23"
vico-charts = "1.16.0" vico-charts = "2.0.1"
zelory = "3.0.1" zelory = "3.0.1"
zoomable = "2.0.0" zoomable = "2.0.0"
zxing = "3.5.3" zxing = "3.5.3"