mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-29 11:11:44 +01:00
Adds reaction watch.
This commit is contained in:
parent
6aadf8a883
commit
cb42196889
@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 167
|
||||
versionName "0.49.4"
|
||||
versionCode 168
|
||||
versionName "0.50.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@ -177,6 +177,12 @@ dependencies {
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.0.0')
|
||||
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
||||
|
||||
// Charts
|
||||
implementation "com.patrykandpatrick.vico:core:${vico_version}"
|
||||
implementation "com.patrykandpatrick.vico:compose:${vico_version}"
|
||||
implementation "com.patrykandpatrick.vico:views:${vico_version}"
|
||||
implementation "com.patrykandpatrick.vico:compose-m2:${vico_version}"
|
||||
|
||||
// Automatic memory leak detection
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
||||
|
||||
|
@ -95,7 +95,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
BadgeAwardEvent.kind
|
||||
),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
limit = 400,
|
||||
limit = 4000,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
|
@ -0,0 +1,245 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
fun UserReactionsRow(model: UserReactionsViewModel, accountViewModel: AccountViewModel, navController: NavController, onClick: () -> Unit) {
|
||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.clickable(onClick = onClick).padding(10.dp)) {
|
||||
Text(
|
||||
text = "Today",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
modifier = Modifier.width(65.dp)
|
||||
)
|
||||
|
||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
||||
UserReplyReaction(model.replies[model.today])
|
||||
}
|
||||
|
||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
||||
UserBoostReaction(model.boosts[model.today])
|
||||
}
|
||||
|
||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
||||
UserLikeReaction(model.replies[model.today])
|
||||
}
|
||||
|
||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
||||
UserZapReaction(model.zaps[model.today])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UserReactionsViewModel : ViewModel() {
|
||||
var user: User? = null
|
||||
|
||||
var reactions by mutableStateOf<Map<String, Int>>(emptyMap())
|
||||
var boosts by mutableStateOf<Map<String, Int>>(emptyMap())
|
||||
var zaps by mutableStateOf<Map<String, BigDecimal>>(emptyMap())
|
||||
var replies by mutableStateOf<Map<String, Int>>(emptyMap())
|
||||
|
||||
val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
|
||||
val today = sdf.format(LocalDateTime.now())
|
||||
|
||||
fun load(baseUser: User) {
|
||||
user = baseUser
|
||||
reactions = emptyMap()
|
||||
boosts = emptyMap()
|
||||
zaps = emptyMap()
|
||||
replies = emptyMap()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
|
||||
fun formatDate(createAt: Long): String {
|
||||
return sdf.format(
|
||||
Instant.ofEpochSecond(createAt)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime()
|
||||
)
|
||||
}
|
||||
|
||||
fun refreshSuspended() {
|
||||
val currentUser = user?.pubkeyHex ?: return
|
||||
|
||||
val reactions = mutableMapOf<String, Int>()
|
||||
val boosts = mutableMapOf<String, Int>()
|
||||
val zaps = mutableMapOf<String, BigDecimal>()
|
||||
val replies = mutableMapOf<String, Int>()
|
||||
|
||||
LocalCache.notes.values.forEach {
|
||||
val noteEvent = it.event
|
||||
if (noteEvent is ReactionEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser)) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
reactions[netDate] = (reactions[netDate] ?: 0) + 1
|
||||
}
|
||||
}
|
||||
if (noteEvent is RepostEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser)) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
boosts[netDate] = (boosts[netDate] ?: 0) + 1
|
||||
}
|
||||
}
|
||||
if (noteEvent is LnZapEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser)) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
|
||||
}
|
||||
}
|
||||
if (noteEvent is TextNoteEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser)) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
replies[netDate] = (replies[netDate] ?: 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.reactions = reactions
|
||||
this.replies = replies
|
||||
this.zaps = zaps
|
||||
this.boosts = boosts
|
||||
}
|
||||
|
||||
var collectorJob: Job? = null
|
||||
|
||||
init {
|
||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
collectorJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserReplyReaction(
|
||||
replyCount: Int?
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_comment),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.Cyan
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
Text(
|
||||
showCount(replyCount),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserBoostReaction(
|
||||
boostCount: Int?
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
Text(
|
||||
showCount(boostCount),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserLikeReaction(
|
||||
likeCount: Int?
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
Text(
|
||||
showCount(likeCount),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserZapReaction(
|
||||
amount: BigDecimal?
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
showAmount(amount),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
@ -1,25 +1,64 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
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.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
|
||||
import com.patrykandpatrick.vico.compose.axis.vertical.endAxis
|
||||
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
|
||||
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.ComposedChartEntryModel
|
||||
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.patrykandpatrick.vico.core.entry.ChartEntryModel
|
||||
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
|
||||
import com.patrykandpatrick.vico.core.entry.composed.plus
|
||||
import com.patrykandpatrick.vico.core.entry.entryOf
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.UserReactionsRow
|
||||
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.note.showCount
|
||||
import com.vitorpamplona.amethyst.ui.screen.CardFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun NotificationScreen(
|
||||
@ -61,6 +100,7 @@ fun NotificationScreen(
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
SummaryBar(accountViewModel, navController)
|
||||
CardFeedView(
|
||||
viewModel = notifFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
@ -72,3 +112,143 @@ fun NotificationScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SummaryBar(accountViewModel: AccountViewModel, navController: NavController) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val accountUser = remember(accountState) { accountState?.account?.userProfile() } ?: return
|
||||
|
||||
val model: UserReactionsViewModel = viewModel()
|
||||
|
||||
var chartModel by remember(accountState) { mutableStateOf<ComposedChartEntryModel<ChartEntryModel>?>(null) }
|
||||
var axisLabels by remember(accountState) { mutableStateOf<List<String>>(emptyList()) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var showChart by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LaunchedEffect(accountUser.pubkeyHex) {
|
||||
scope.launch {
|
||||
model.load(accountUser)
|
||||
model.refreshSuspended()
|
||||
val day = 24 * 60 * 60L
|
||||
val now = LocalDateTime.now()
|
||||
val displayAxisFormatter = DateTimeFormatter.ofPattern("EEE")
|
||||
|
||||
val dataAxisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { model.sdf.format(now.minusSeconds(day * it)) }
|
||||
axisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { displayAxisFormatter.format(now.minusSeconds(day * it)) }
|
||||
|
||||
val listOfCountCurves = listOf(
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.replies[dateStr]?.toFloat() ?: 0f) },
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.boosts[dateStr]?.toFloat() ?: 0f) },
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.reactions[dateStr]?.toFloat() ?: 0f) }
|
||||
)
|
||||
|
||||
val listOfValueCurves = listOf(
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.zaps[dateStr]?.toFloat() ?: 0f) }
|
||||
)
|
||||
|
||||
val chartEntryModelProducer1 = ChartEntryModelProducer(listOfCountCurves).getModel()
|
||||
val chartEntryModelProducer2 = ChartEntryModelProducer(listOfValueCurves).getModel()
|
||||
|
||||
chartModel = chartEntryModelProducer1.plus(chartEntryModelProducer2)
|
||||
}
|
||||
}
|
||||
|
||||
UserReactionsRow(model, accountViewModel, navController) {
|
||||
showChart = !showChart
|
||||
}
|
||||
|
||||
val lineChartCount =
|
||||
lineChart(
|
||||
lines = listOf(Color.Cyan, 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
|
||||
)
|
||||
|
||||
chartModel?.let {
|
||||
if (showChart) {
|
||||
Row(modifier = Modifier.padding(vertical = 10.dp, horizontal = 20.dp).clickable(onClick = { showChart = !showChart })) {
|
||||
ProvideChartStyle() {
|
||||
Chart(
|
||||
chart = remember(lineChartCount, lineChartZaps) {
|
||||
lineChartCount.plus(lineChartZaps)
|
||||
},
|
||||
model = it,
|
||||
startAxis = startAxis(
|
||||
valueFormatter = CountAxisValueFormatter()
|
||||
),
|
||||
endAxis = endAxis(
|
||||
valueFormatter = AmountAxisValueFormatter()
|
||||
),
|
||||
bottomAxis = bottomAxis(
|
||||
valueFormatter = LabelValueFormatter(axisLabels)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
|
||||
class LabelValueFormatter(val axisLabels: List<String>) : AxisValueFormatter<AxisPosition.Horizontal.Bottom> {
|
||||
override fun formatValue(
|
||||
value: Float,
|
||||
chartValues: ChartValues
|
||||
): String {
|
||||
return axisLabels[value.roundToInt()]
|
||||
}
|
||||
}
|
||||
|
||||
class CountAxisValueFormatter() : AxisValueFormatter<AxisPosition.Vertical.Start> {
|
||||
override fun formatValue(
|
||||
value: Float,
|
||||
chartValues: ChartValues
|
||||
): String {
|
||||
return showCount(value.roundToInt())
|
||||
}
|
||||
}
|
||||
|
||||
class AmountAxisValueFormatter() : AxisValueFormatter<AxisPosition.Vertical.End> {
|
||||
override fun formatValue(
|
||||
value: Float,
|
||||
chartValues: ChartValues
|
||||
): String {
|
||||
return showAmount(value.toBigDecimal())
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ buildscript {
|
||||
room_version = "2.4.3"
|
||||
accompanist_version = '0.30.0'
|
||||
coil_version = '2.3.0'
|
||||
vico_version = '1.6.5'
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
|
Loading…
x
Reference in New Issue
Block a user