fix: stabilize DVM heartbeat freshness state and label the offline dot

This commit is contained in:
Believethehype
2026-09-10 12:52:19 +02:00
parent 97172c1bb3
commit ad1d4ccf8b
4 changed files with 36 additions and 9 deletions
@@ -52,6 +52,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
@@ -71,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.dvm_offline
import com.vitorpamplona.amethyst.commons.resources.feed_filter_select_an_option
import com.vitorpamplona.amethyst.commons.resources.feed_filter_selected
import com.vitorpamplona.amethyst.commons.resources.lack_location_permissions
@@ -181,11 +183,18 @@ fun FeedFilterSpinner(
val favoriteAlgoFeedAddress = (selected?.name as? FavoriteAlgoFeedName)?.note?.address
if (favoriteAlgoFeedAddress != null) {
val heartbeatFresh by rememberDvmHeartbeatFresh(favoriteAlgoFeedAddress, accountViewModel)
val offlineLabel = stringRes(Res.string.dvm_offline)
Text(
text = if (heartbeatFresh) currentText else "$currentText \u2022",
color = if (heartbeatFresh) Color.Unspecified else MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
if (heartbeatFresh) {
Modifier
} else {
Modifier.semantics { contentDescription = "$currentText, $offlineLabel" }
},
)
} else {
Text(
@@ -390,6 +399,7 @@ fun RenderOption(
} else {
true
}
val offlineLabel = stringRes(Res.string.dvm_offline)
Text(
text = if (heartbeatFresh) name else "$name \u2022",
fontSize = Font14SP,
@@ -399,6 +409,12 @@ fun RenderOption(
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
modifier =
if (heartbeatFresh) {
Modifier
} else {
Modifier.semantics { contentDescription = "$name, $offlineLabel" }
},
)
}
@@ -78,19 +78,18 @@ fun rememberDvmHeartbeatFresh(
}
}
val resolved = heartbeatNote ?: return remember(heartbeatAddressTag) { mutableStateOf(false) }
val observed =
heartbeatNote?.let { observeNoteAndMap(it, accountViewModel) { it.event as? DvmHeartbeatEvent } }
val heartbeat by observeNoteAndMap(resolved, accountViewModel) { it.event as? DvmHeartbeatEvent }
var now by remember(resolved) { mutableLongStateOf(TimeUtils.now()) }
LaunchedEffect(resolved) {
var now by remember(heartbeatNote) { mutableLongStateOf(TimeUtils.now()) }
LaunchedEffect(heartbeatNote) {
while (isActive) {
delay(HEARTBEAT_RECHECK_MILLIS)
now = TimeUtils.now()
}
}
val beat = heartbeat
val beat = observed?.value
return rememberUpdatedState(beat != null && beat.isFreshAt(now))
}
@@ -52,6 +52,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
@@ -67,6 +68,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.dvm_offline
import com.vitorpamplona.amethyst.commons.resources.favorite_dvms_add_more
import com.vitorpamplona.amethyst.commons.resources.favorite_dvms_empty_cta
import com.vitorpamplona.amethyst.commons.resources.favorite_dvms_empty_headline
@@ -285,12 +287,19 @@ private fun FavoriteAlgoFeedRow(
) {
val heartbeatFresh by rememberDvmHeartbeatFresh(feedNote.address, accountViewModel)
val displayName = card.name.ifBlank { feedNote.dTag() }
val offlineLabel = stringRes(Res.string.dvm_offline)
Text(
text = if (heartbeatFresh) displayName else "$displayName \u2022",
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
modifier =
if (heartbeatFresh) {
Modifier
} else {
Modifier.semantics { contentDescription = "$displayName, $offlineLabel" }
},
)
card.description?.takeIf { it.isNotBlank() }?.let {
Spacer(modifier = StdVertSpacer)
@@ -200,10 +200,13 @@ private fun AllFavoriteAlgoFeedsBanner(
if (addresses.isEmpty()) return
// Map every address to its freshness first (non-short-circuiting) so per-address
// composable call sites stay stable instead of appearing/disappearing with freshness.
val anyHeartbeatFresh =
addresses.any { address ->
rememberDvmHeartbeatFresh(address, accountViewModel).value
}
addresses
.map { address ->
rememberDvmHeartbeatFresh(address, accountViewModel).value
}.any { it }
if (!anyHeartbeatFresh) {
BannerCard(modifier) {
BannerMessageRow(