Skip to content

Commit eb752eb

Browse files
psjostromclaude
andauthored
feat(ui): pause-all alerts shortcut + monthly story revisit (#211)
* refactor(stats): extract StoryEntryCard composable * test(stats): add unit tests for StoryEntryCard * feat(stats): keep monthly story card visible after viewing in muted form * feat(alerts): add Pause all alerts shortcut to PauseAlertsSheet * i18n(alerts): translate pause_all_alerts to de/es/fr/sv * docs: cover Pause all shortcut and story revisit * refactor(ui): tighten pause-all wiring and gate story card on resolved state - PauseAlertsSheet owns the per-category fan-out internally via AlertCategory.entries; drops the onPauseAll parameter and the identical lambda duplicated at both call sites. - PauseAllRow surfaces the "Includes urgent low alerts" warning at the decision point, matching the per-LOW row. - StatsScreen.storyViewedMonth becomes nullable so the monthly story card waits for DataStore to resolve instead of flashing highlighted before going muted for already-viewed users. - Extract MonthlyStoryEntry into ui/components/ to make the visibility/loading logic testable; add tests covering the regression (card persists after viewing) and the loading-null branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui(alerts): drop urgent-low subtitles, rename rows, reorder All/High/Low The pause sheet now relies on the row headers themselves to convey scope. Header text becomes "All alerts" / "All high alerts" / "All low alerts" in that order; the "Includes urgent low alerts" subtitle is removed from both the All-alerts row and the per-LOW row. Drops the unused pause_low_warning string in en/de/es/fr/sv and the warning parameter on PauseCategoryRow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3851af8 commit eb752eb

15 files changed

Lines changed: 392 additions & 107 deletions

File tree

app/src/main/java/com/psjostrom/strimma/ui/StatsScreen.kt

Lines changed: 8 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package com.psjostrom.strimma.ui
22

3-
import androidx.compose.foundation.BorderStroke
43
import androidx.compose.foundation.Canvas
54
import androidx.compose.foundation.layout.*
65
import androidx.compose.foundation.rememberScrollState
76
import androidx.compose.foundation.shape.RoundedCornerShape
87
import androidx.compose.foundation.verticalScroll
98
import androidx.compose.material.icons.Icons
109
import androidx.compose.material.icons.automirrored.filled.ArrowBack
11-
import androidx.compose.material.icons.automirrored.filled.ArrowForward
1210
import androidx.compose.material.icons.outlined.Share
1311
import androidx.compose.material3.*
1412
import androidx.compose.runtime.*
@@ -17,7 +15,6 @@ import androidx.compose.ui.Modifier
1715
import androidx.compose.ui.geometry.CornerRadius
1816
import androidx.compose.ui.geometry.Offset
1917
import androidx.compose.ui.geometry.Size
20-
import androidx.compose.ui.platform.LocalConfiguration
2118
import androidx.compose.ui.platform.LocalContext
2219
import androidx.compose.ui.res.stringResource
2320
import androidx.compose.ui.text.font.FontWeight
@@ -29,7 +26,7 @@ import com.psjostrom.strimma.data.AgpResult
2926
import com.psjostrom.strimma.data.GlucoseReading
3027
import com.psjostrom.strimma.data.GlucoseStats
3128
import com.psjostrom.strimma.data.GlucoseUnit
32-
import com.psjostrom.strimma.data.story.toMillisRange
29+
import com.psjostrom.strimma.ui.components.MonthlyStoryEntry
3330
import com.psjostrom.strimma.data.HbA1cUnit
3431
import com.psjostrom.strimma.data.StatsCalculator
3532
import com.psjostrom.strimma.data.Treatment
@@ -76,7 +73,7 @@ fun StatsScreen(
7673
mealTimeSlotConfig: MealTimeSlotConfig,
7774
onExportCsv: suspend (Int) -> String,
7875
onNavigateToStory: ((Int, Int) -> Unit)? = null,
79-
storyViewedMonth: String = "",
76+
storyViewedMonth: String? = null,
8077
onBack: (() -> Unit)? = null
8178
) {
8279
val bg = MaterialTheme.colorScheme.background
@@ -150,57 +147,13 @@ fun StatsScreen(
150147
) {
151148
Spacer(modifier = Modifier.height(4.dp))
152149

153-
// Monthly Story entry card — last completed month only, hidden if insufficient data or already viewed
150+
// Monthly Story entry card — last completed month only, hidden if insufficient data
154151
onNavigateToStory?.let { navigate ->
155-
val lastMonth = java.time.YearMonth.now().minusMonths(1)
156-
val lastMonthKey = "%d-%02d".format(lastMonth.year, lastMonth.monthValue)
157-
var hasData by remember { mutableStateOf(false) }
158-
LaunchedEffect(Unit) {
159-
val zone = java.time.ZoneId.systemDefault()
160-
val (start, end) = lastMonth.toMillisRange(zone)
161-
val hoursAgo = ((System.currentTimeMillis() - start) / 3_600_000L).toInt()
162-
val readings = onLoadReadings(hoursAgo)
163-
val inMonth = readings.filter { it.ts in start..end }
164-
val days = inMonth.map {
165-
java.time.Instant.ofEpochMilli(it.ts).atZone(zone).toLocalDate()
166-
}.distinct().size
167-
hasData = days >= 7
168-
}
169-
if (hasData && storyViewedMonth != lastMonthKey) {
170-
val monthName = lastMonth.month.getDisplayName(
171-
java.time.format.TextStyle.FULL, LocalConfiguration.current.locales[0]
172-
)
173-
Surface(
174-
onClick = { navigate(lastMonth.year, lastMonth.monthValue) },
175-
modifier = Modifier.fillMaxWidth(),
176-
shape = MaterialTheme.shapes.medium,
177-
color = MaterialTheme.colorScheme.surfaceVariant,
178-
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
179-
) {
180-
Row(
181-
Modifier.padding(16.dp),
182-
horizontalArrangement = Arrangement.SpaceBetween,
183-
verticalAlignment = Alignment.CenterVertically
184-
) {
185-
Column(modifier = Modifier.weight(1f)) {
186-
Text(
187-
stringResource(R.string.story_entry_title, monthName),
188-
style = MaterialTheme.typography.titleSmall
189-
)
190-
Text(
191-
stringResource(R.string.story_entry_subtitle),
192-
style = MaterialTheme.typography.bodySmall,
193-
color = MaterialTheme.colorScheme.onSurfaceVariant
194-
)
195-
}
196-
Icon(
197-
Icons.AutoMirrored.Filled.ArrowForward,
198-
contentDescription = null,
199-
tint = MaterialTheme.colorScheme.onSurfaceVariant
200-
)
201-
}
202-
}
203-
}
152+
MonthlyStoryEntry(
153+
storyViewedMonth = storyViewedMonth,
154+
onLoadReadings = onLoadReadings,
155+
onNavigate = navigate
156+
)
204157
}
205158

206159
// Tab selector

app/src/main/java/com/psjostrom/strimma/ui/StrimmaNavGraph.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ fun StrimmaNavGraph(
511511
initial = com.psjostrom.strimma.data.HbA1cUnit.MMOL_MOL
512512
)
513513
val tauMinutes by viewModel.tauMinutes.collectAsState()
514-
val storyViewedMonth by viewModel.settings.storyViewedMonth.collectAsState(initial = "")
514+
val storyViewedMonth by viewModel.settings.storyViewedMonth.collectAsState(initial = null)
515515
StatsScreen(
516516
bgLow = bgLow,
517517
bgHigh = bgHigh,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.psjostrom.strimma.ui.components
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.getValue
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.setValue
9+
import androidx.compose.ui.platform.LocalConfiguration
10+
import com.psjostrom.strimma.data.GlucoseReading
11+
import com.psjostrom.strimma.data.story.toMillisRange
12+
import java.time.Instant
13+
import java.time.YearMonth
14+
import java.time.ZoneId
15+
import java.time.format.TextStyle
16+
17+
private const val MIN_DAYS_FOR_STORY = 7
18+
private const val MS_PER_HOUR = 3_600_000L
19+
20+
@Composable
21+
fun MonthlyStoryEntry(
22+
storyViewedMonth: String?,
23+
onLoadReadings: suspend (Int) -> List<GlucoseReading>,
24+
onNavigate: (Int, Int) -> Unit
25+
) {
26+
val lastMonth = remember { YearMonth.now().minusMonths(1) }
27+
val lastMonthKey = remember(lastMonth) { "%d-%02d".format(lastMonth.year, lastMonth.monthValue) }
28+
var hasData by remember { mutableStateOf(false) }
29+
LaunchedEffect(Unit) {
30+
val zone = ZoneId.systemDefault()
31+
val (start, end) = lastMonth.toMillisRange(zone)
32+
val hoursAgo = ((System.currentTimeMillis() - start) / MS_PER_HOUR).toInt()
33+
val readings = onLoadReadings(hoursAgo)
34+
val days = readings.filter { it.ts in start..end }
35+
.map { Instant.ofEpochMilli(it.ts).atZone(zone).toLocalDate() }
36+
.distinct()
37+
.size
38+
hasData = days >= MIN_DAYS_FOR_STORY
39+
}
40+
if (hasData && storyViewedMonth != null) {
41+
val monthName = lastMonth.month.getDisplayName(
42+
TextStyle.FULL, LocalConfiguration.current.locales[0]
43+
)
44+
StoryEntryCard(
45+
monthName = monthName,
46+
viewed = storyViewedMonth == lastMonthKey,
47+
onClick = { onNavigate(lastMonth.year, lastMonth.monthValue) }
48+
)
49+
}
50+
}

app/src/main/java/com/psjostrom/strimma/ui/components/PauseAlertsSheet.kt

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,9 @@ fun PauseAlertsSheetContent(
9292
modifier = Modifier.padding(bottom = 20.dp)
9393
)
9494

95-
PauseCategoryRow(
96-
label = stringResource(R.string.pause_low_alerts),
97-
color = BelowLow,
98-
expiryMs = pauseLowExpiryMs,
99-
category = AlertCategory.LOW,
100-
onPause = onPause,
101-
onCancel = onCancel,
102-
warning = stringResource(R.string.pause_low_warning)
103-
)
95+
PauseAllRow(onPause = onPause)
10496

105-
Spacer(modifier = Modifier.height(16.dp))
97+
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
10698

10799
PauseCategoryRow(
108100
label = stringResource(R.string.pause_high_alerts),
@@ -112,6 +104,48 @@ fun PauseAlertsSheetContent(
112104
onPause = onPause,
113105
onCancel = onCancel
114106
)
107+
108+
Spacer(modifier = Modifier.height(16.dp))
109+
110+
PauseCategoryRow(
111+
label = stringResource(R.string.pause_low_alerts),
112+
color = BelowLow,
113+
expiryMs = pauseLowExpiryMs,
114+
category = AlertCategory.LOW,
115+
onPause = onPause,
116+
onCancel = onCancel
117+
)
118+
}
119+
}
120+
121+
@Composable
122+
private fun PauseAllRow(
123+
onPause: (AlertCategory, Long) -> Unit
124+
) {
125+
Column {
126+
Text(
127+
text = stringResource(R.string.pause_all_alerts),
128+
color = MaterialTheme.colorScheme.onSurface,
129+
fontSize = 14.sp,
130+
fontWeight = FontWeight.SemiBold,
131+
modifier = Modifier.padding(bottom = 8.dp)
132+
)
133+
Row(
134+
horizontalArrangement = Arrangement.spacedBy(6.dp)
135+
) {
136+
DURATIONS.forEach { (durationMs, labelRes) ->
137+
FilledTonalButton(
138+
onClick = {
139+
AlertCategory.entries.forEach { cat -> onPause(cat, durationMs) }
140+
},
141+
shape = RoundedCornerShape(8.dp),
142+
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp),
143+
modifier = Modifier.height(36.dp)
144+
) {
145+
Text(stringResource(labelRes), fontSize = 13.sp)
146+
}
147+
}
148+
}
115149
}
116150
}
117151

@@ -122,27 +156,17 @@ private fun PauseCategoryRow(
122156
expiryMs: Long?,
123157
category: AlertCategory,
124158
onPause: (AlertCategory, Long) -> Unit,
125-
onCancel: (AlertCategory) -> Unit,
126-
warning: String? = null
159+
onCancel: (AlertCategory) -> Unit
127160
) {
128161
Column {
129162
Text(
130163
text = label,
131164
color = color,
132165
fontSize = 14.sp,
133166
fontWeight = FontWeight.SemiBold,
134-
modifier = Modifier.padding(bottom = if (warning != null && expiryMs == null) 2.dp else 8.dp)
167+
modifier = Modifier.padding(bottom = 8.dp)
135168
)
136169

137-
if (warning != null && (expiryMs == null || expiryMs <= System.currentTimeMillis())) {
138-
Text(
139-
text = warning,
140-
color = MaterialTheme.colorScheme.onSurfaceVariant,
141-
fontSize = 12.sp,
142-
modifier = Modifier.padding(bottom = 8.dp)
143-
)
144-
}
145-
146170
if (expiryMs != null && expiryMs > System.currentTimeMillis()) {
147171
// Active pause — show countdown + cancel
148172
Row(
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.psjostrom.strimma.ui.components
2+
3+
import androidx.compose.foundation.BorderStroke
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.material.icons.Icons
10+
import androidx.compose.material.icons.automirrored.filled.ArrowForward
11+
import androidx.compose.material3.Icon
12+
import androidx.compose.material3.MaterialTheme
13+
import androidx.compose.material3.Surface
14+
import androidx.compose.material3.Text
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.ui.Alignment
17+
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.res.stringResource
19+
import androidx.compose.ui.unit.dp
20+
import com.psjostrom.strimma.R
21+
22+
@Composable
23+
fun StoryEntryCard(
24+
monthName: String,
25+
viewed: Boolean,
26+
onClick: () -> Unit,
27+
modifier: Modifier = Modifier
28+
) {
29+
Surface(
30+
onClick = onClick,
31+
modifier = modifier.fillMaxWidth(),
32+
shape = MaterialTheme.shapes.medium,
33+
color = if (viewed) {
34+
MaterialTheme.colorScheme.surface
35+
} else {
36+
MaterialTheme.colorScheme.surfaceVariant
37+
},
38+
border = if (viewed) {
39+
null
40+
} else {
41+
BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
42+
}
43+
) {
44+
Row(
45+
Modifier.padding(16.dp),
46+
horizontalArrangement = Arrangement.SpaceBetween,
47+
verticalAlignment = Alignment.CenterVertically
48+
) {
49+
Column(modifier = Modifier.weight(1f)) {
50+
Text(
51+
stringResource(R.string.story_entry_title, monthName),
52+
style = MaterialTheme.typography.titleSmall
53+
)
54+
Text(
55+
stringResource(R.string.story_entry_subtitle),
56+
style = MaterialTheme.typography.bodySmall,
57+
color = MaterialTheme.colorScheme.onSurfaceVariant
58+
)
59+
}
60+
Icon(
61+
Icons.AutoMirrored.Filled.ArrowForward,
62+
contentDescription = null,
63+
tint = MaterialTheme.colorScheme.onSurfaceVariant
64+
)
65+
}
66+
}
67+
}

app/src/main/res/values-de/strings.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,9 @@
414414

415415
<!-- Alert pause -->
416416
<string name="pause_alerts">Alarme pausieren</string>
417-
<string name="pause_low_alerts">Tief-Alarme</string>
418-
<string name="pause_high_alerts">Hoch-Alarme</string>
417+
<string name="pause_all_alerts">Alle Alarme</string>
418+
<string name="pause_low_alerts">Alle Tief-Alarme</string>
419+
<string name="pause_high_alerts">Alle Hoch-Alarme</string>
419420
<string name="pause_duration_30m">30m</string>
420421
<string name="pause_duration_1h">1h</string>
421422
<string name="pause_duration_1_5h">1,5h</string>
@@ -427,7 +428,6 @@
427428
<string name="pause_low_paused">Tief-Alarme pausiert</string>
428429
<string name="pause_high_paused">Hoch-Alarme pausiert</string>
429430
<string name="pause_countdown">Pausiert · %1$s</string>
430-
<string name="pause_low_warning">Einschließlich Akut-tief-Alarm</string>
431431

432432
<string name="settings_sharing">Teilen</string>
433433
<string name="settings_sharing_subtitle">Broadcast, Webserver, Export</string>

app/src/main/res/values-es/strings.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,9 @@
414414

415415
<!-- Alert pause -->
416416
<string name="pause_alerts">Pausar alertas</string>
417-
<string name="pause_low_alerts">Alertas de bajo</string>
418-
<string name="pause_high_alerts">Alertas de alto</string>
417+
<string name="pause_all_alerts">Todas las alertas</string>
418+
<string name="pause_low_alerts">Todas las alertas de bajo</string>
419+
<string name="pause_high_alerts">Todas las alertas de alto</string>
419420
<string name="pause_duration_30m">30m</string>
420421
<string name="pause_duration_1h">1h</string>
421422
<string name="pause_duration_1_5h">1,5h</string>
@@ -427,7 +428,6 @@
427428
<string name="pause_low_paused">Alertas de bajo pausadas</string>
428429
<string name="pause_high_paused">Alertas de alto pausadas</string>
429430
<string name="pause_countdown">Pausado · %1$s</string>
430-
<string name="pause_low_warning">Incluye alerta de bajo urgente</string>
431431

432432
<string name="settings_sharing">Compartir</string>
433433
<string name="settings_sharing_subtitle">Broadcast, servidor web, exportar</string>

app/src/main/res/values-fr/strings.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,9 @@
414414

415415
<!-- Alert pause -->
416416
<string name="pause_alerts">Suspendre les alertes</string>
417-
<string name="pause_low_alerts">Alertes basses</string>
418-
<string name="pause_high_alerts">Alertes hautes</string>
417+
<string name="pause_all_alerts">Toutes les alertes</string>
418+
<string name="pause_low_alerts">Toutes les alertes basses</string>
419+
<string name="pause_high_alerts">Toutes les alertes hautes</string>
419420
<string name="pause_duration_30m">30m</string>
420421
<string name="pause_duration_1h">1h</string>
421422
<string name="pause_duration_1_5h">1,5h</string>
@@ -427,7 +428,6 @@
427428
<string name="pause_low_paused">Alertes basses suspendues</string>
428429
<string name="pause_high_paused">Alertes hautes suspendues</string>
429430
<string name="pause_countdown">Suspendu · %1$s</string>
430-
<string name="pause_low_warning">Inclut l\u0027alerte bas urgent</string>
431431

432432
<string name="settings_sharing">Partage</string>
433433
<string name="settings_sharing_subtitle">Broadcast, serveur web, export</string>

0 commit comments

Comments
 (0)