feat(marmot): surface per-relay freshness on group info screen
The compact GroupRelayStrip at the top of the screen uses a coloured dot to indicate whether each relay has carried any kind:445 traffic in the last 7 days, but nothing tells the user what the dot means or how stale a given relay actually is. Adds a detailed "Relays" section to the scrollable body that lists each group relay with its URL, status dot, and a "last event 3m ago" line derived from the existing relayActivity StateFlow on MarmotGroupChatroom. Rows click through to the existing RelayInfo route so users can inspect NIP-11 metadata, just like the strip. Uses the shared timeAgo helper for consistent formatting with the rest of the app. https://claude.ai/code/session_01Unm6uLHGLj9UcBY7hWfJVW
This commit is contained in:
+110
@@ -84,6 +84,7 @@ import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.theme.MediumRelayIconModifier
|
||||
@@ -219,6 +220,17 @@ fun MarmotGroupInfoScreen(
|
||||
|
||||
// Members list (scrollable, takes remaining vertical space)
|
||||
LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||
if (groupRelays.isNotEmpty()) {
|
||||
item {
|
||||
RelayHealthSection(
|
||||
relayUrls = groupRelays,
|
||||
relayActivity = relayActivity,
|
||||
nav = nav,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Members",
|
||||
@@ -790,3 +802,101 @@ fun GroupRelayTile(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed per-relay listing showing URL, connection-status dot, and the
|
||||
* freshness of the most recent kind:445 group event we've received from
|
||||
* each relay. Complements the compact [GroupRelayStrip] at the top of the
|
||||
* screen — that one is an at-a-glance visual; this one surfaces the
|
||||
* underlying data so users can diagnose which relay is lagging.
|
||||
*/
|
||||
@Composable
|
||||
fun RelayHealthSection(
|
||||
relayUrls: List<String>,
|
||||
relayActivity: Map<NormalizedRelayUrl, Long>,
|
||||
nav: INav,
|
||||
) {
|
||||
val normalized =
|
||||
remember(relayUrls) {
|
||||
relayUrls.mapNotNull { url -> url.normalizeRelayUrlOrNull()?.let { url to it } }
|
||||
}
|
||||
if (normalized.isEmpty()) return
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "Relays",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
)
|
||||
val nowSeconds = System.currentTimeMillis() / 1000L
|
||||
normalized.forEach { (raw, relay) ->
|
||||
val lastSeen = relayActivity[relay]
|
||||
val isActive = lastSeen != null && (nowSeconds - lastSeen) <= RELAY_ACTIVITY_WINDOW_SECS
|
||||
RelayHealthRow(
|
||||
relay = relay,
|
||||
fallbackUrl = raw,
|
||||
lastSeen = lastSeen,
|
||||
isActive = isActive,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayHealthRow(
|
||||
relay: NormalizedRelayUrl,
|
||||
fallbackUrl: String,
|
||||
lastSeen: Long?,
|
||||
isActive: Boolean,
|
||||
nav: INav,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val dotColor =
|
||||
if (isActive) {
|
||||
MaterialTheme.colorScheme.allGoodColor
|
||||
} else {
|
||||
MaterialTheme.colorScheme.placeholderText
|
||||
}
|
||||
val subtitle =
|
||||
if (lastSeen == null) {
|
||||
"no events yet"
|
||||
} else {
|
||||
"last event${timeAgo(lastSeen, context)}"
|
||||
}
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { nav.nav(Route.RelayInfo(relay.url)) }
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(dotColor),
|
||||
)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 12.dp)
|
||||
.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = relay.url.ifEmpty { fallbackUrl },
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user