mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 11:48:24 +00:00
Improves markers
This commit is contained in:
+14
-3
@@ -31,7 +31,9 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -41,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachDetailDialog
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachSentinels
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState
|
||||
@@ -193,12 +196,12 @@ private fun FeedLoaded(
|
||||
buildList {
|
||||
if (!giftWrapsExhausted) {
|
||||
giftWrapsProgress.forEach { (relay, p) ->
|
||||
add(RelayReachCursor("17:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { giftWrapsHistory.advance(relay) })
|
||||
add(RelayReachCursor("17:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p), "NIP-17") { giftWrapsHistory.advance(relay) })
|
||||
}
|
||||
}
|
||||
if (!nip04Exhausted) {
|
||||
nip04Progress.forEach { (relay, p) ->
|
||||
add(RelayReachCursor("04:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { nip04History.advance(relay) })
|
||||
add(RelayReachCursor("04:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p), "NIP-04") { nip04History.advance(relay) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,6 +211,14 @@ private fun FeedLoaded(
|
||||
// (a live DM bumping a room) no longer re-fire paging. The markers below are pure UI.
|
||||
RelayReachSentinels(limits, listState) { index -> items.list.getOrNull(index)?.createdAt() }
|
||||
|
||||
// The relays behind a tapped in-stream "Relay sync" marker; non-null shows the per-relay popup so the
|
||||
// terse divider isn't a dead end — every count/name is one tap from the full breakdown (which relays,
|
||||
// protocol, how far back each paged).
|
||||
var syncDetail by remember { mutableStateOf<List<RelayReachCursor>?>(null) }
|
||||
syncDetail?.let { detail ->
|
||||
RelayReachDetailDialog(detail, ::formatHistoryReachDate) { syncDetail = null }
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
state = listState,
|
||||
@@ -244,7 +255,7 @@ private fun FeedLoaded(
|
||||
limits,
|
||||
item.createdAt(),
|
||||
items.list.getOrNull(index + 1)?.createdAt(),
|
||||
)
|
||||
) { syncDetail = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,17 +56,21 @@
|
||||
<string name="accessibility_navigate">Navigate</string>
|
||||
|
||||
<!-- Relay history paging (shared feed markers + status card) -->
|
||||
<string name="chats_history_relay_sync">Relay sync:</string>
|
||||
<string name="chats_history_loading_label">Loading:</string>
|
||||
<string name="chats_history_fully_loaded_label">Fully loaded:</string>
|
||||
<string name="chats_history_fully_loaded">(fully loaded)</string>
|
||||
<string name="chats_history_by_relay">History by relay</string>
|
||||
<string name="chats_history_stalled_retry">Will retry when you reopen this screen</string>
|
||||
<string name="chats_history_older">Older %1$s messages</string>
|
||||
<string name="chats_history_all_caught_up">All caught up</string>
|
||||
<string name="chats_history_reached_start">Reached the start of your %1$s messages</string>
|
||||
<string name="chats_history_subtitle">%1$s · %2$s · back to %3$s</string>
|
||||
<string name="chats_history_subtitle">%1$s · %2$s · loaded since %3$s</string>
|
||||
<string name="chats_history_subtitle_no_date">%1$s · %2$s</string>
|
||||
<string name="chats_history_waiting">waiting on %1$s</string>
|
||||
<string name="chats_history_incomplete">Some relays didn\'t respond</string>
|
||||
<string name="chats_history_incomplete_sub">%1$s unreachable · tap to see which</string>
|
||||
<string name="chats_history_relays_title">%1$s · history by relay</string>
|
||||
<string name="chats_history_relay_back">back to %1$s</string>
|
||||
<string name="chats_history_relay_since">since %1$s</string>
|
||||
<string name="action_dismiss">Dismiss</string>
|
||||
<plurals name="chats_history_relays">
|
||||
<item quantity="one">%1$d relay</item>
|
||||
|
||||
+74
-63
@@ -60,14 +60,15 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.action_dismiss
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_all_caught_up
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_by_relay
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_incomplete
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_incomplete_sub
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_older
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_reached_start
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_relay_back
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_relay_sync
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_relay_since
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_relays
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_relays_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_stalled_retry
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_subtitle
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_subtitle_no_date
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_waiting
|
||||
@@ -277,7 +278,8 @@ fun historySubtitle(
|
||||
|
||||
/**
|
||||
* Popup shown when the history card is tapped: one row per relay with its state glyph (✓ done, … stalled,
|
||||
* ↓ still reaching) and how far back it has paged ("back to <date>"), deepest-reaching first.
|
||||
* ↓ still reaching) and how far back it has paged ("since <date>"), deepest-reaching first. A stalled relay
|
||||
* also gets a one-line hint that it retries when the screen is reopened.
|
||||
*/
|
||||
@Composable
|
||||
fun DmHistoryRelayDialog(
|
||||
@@ -300,32 +302,30 @@ fun DmHistoryRelayDialog(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
rows.forEach { (relay, p) ->
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = relayStateGlyph(p),
|
||||
color = relayStateColor(p),
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.width(22.dp),
|
||||
)
|
||||
Text(
|
||||
text = relayShortName(relay),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.chats_history_relay_back, formatReachDate(p.reachedUntil)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = relayStateGlyph(p),
|
||||
color = relayStateColor(p),
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.width(22.dp),
|
||||
)
|
||||
Text(
|
||||
text = relayShortName(relay),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.chats_history_relay_since, formatReachDate(p.reachedUntil)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
if (p.stalled) StalledRetryHint()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,6 +333,19 @@ fun DmHistoryRelayDialog(
|
||||
)
|
||||
}
|
||||
|
||||
/** The "stopped early, retries on reopen" caption shown under a stalled relay in the per-relay popups.
|
||||
* Retry is demand-driven (no timer): reopening the screen re-binds and clears the stalled set, which
|
||||
* retries it — so that's what we tell the user rather than a countdown we can't honour. */
|
||||
@Composable
|
||||
private fun StalledRetryHint() {
|
||||
Text(
|
||||
text = stringResource(Res.string.chats_history_stalled_retry),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(start = 22.dp, top = 2.dp),
|
||||
)
|
||||
}
|
||||
|
||||
private fun relayStateGlyph(p: RelayPagingProgress) =
|
||||
when {
|
||||
p.done -> "✓"
|
||||
@@ -355,10 +368,10 @@ private fun relayShortName(relay: NormalizedRelayUrl): String =
|
||||
.substringBefore('/')
|
||||
|
||||
/**
|
||||
* Popup shown when an in-stream "Relay sync" marker is tapped: the relays whose window sits at that point
|
||||
* Popup shown when an in-stream "Loading" marker is tapped: the relays whose window sits at that point
|
||||
* in the stream, each with its protocol tag, state glyph (✓ done · … stalled · ↓ reaching) and how far
|
||||
* back it has paged — so the otherwise-terse `Relay sync: ✓ N` divider stops being a dead end and its
|
||||
* meaning is explorable. Deepest-reaching first.
|
||||
* back it has paged — so the otherwise-terse `Loading: ↓ N` divider stops being a dead end and its
|
||||
* meaning is explorable. A stalled relay also gets the retry-on-reopen hint. Deepest-reaching first.
|
||||
*/
|
||||
@Composable
|
||||
fun RelayReachDetailDialog(
|
||||
@@ -372,7 +385,7 @@ fun RelayReachDetailDialog(
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(Res.string.action_dismiss)) }
|
||||
},
|
||||
title = { Text(stringResource(Res.string.chats_history_relay_sync)) },
|
||||
title = { Text(stringResource(Res.string.chats_history_by_relay)) },
|
||||
text = {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -380,41 +393,39 @@ fun RelayReachDetailDialog(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
rows.forEach { c ->
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = reachGlyph(c.state),
|
||||
color = reachColor(c.state),
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.width(22.dp),
|
||||
)
|
||||
if (c.protocol.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = c.protocol,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
text = reachGlyph(c.state),
|
||||
color = reachColor(c.state),
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.width(22.dp),
|
||||
)
|
||||
if (c.protocol.isNotEmpty()) {
|
||||
Text(
|
||||
text = c.protocol,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
}
|
||||
Text(
|
||||
text = c.name,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.chats_history_relay_since, formatReachDate(c.reachedUntil)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
}
|
||||
Text(
|
||||
text = c.name,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.chats_history_relay_back, formatReachDate(c.reachedUntil)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
if (c.state == RelayReachState.STALLED) StalledRetryHint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+67
-9
@@ -42,10 +42,14 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_relay_sync
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_fully_loaded
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_fully_loaded_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_loading_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_relays
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
// A relay-reach divider is hair-thin; inlined here so the shared component carries no app-theme dep.
|
||||
@@ -171,6 +175,13 @@ fun RelayReachSentinels(
|
||||
* (at [newerCreatedAt]) and its next-older neighbour (at [olderCreatedAt], null at the oldest end). Pure
|
||||
* UI: the load driving lives in [RelayReachSentinels], so this can be (re)placed freely per row on
|
||||
* every feed reorder without triggering any paging.
|
||||
*
|
||||
* A [DONE][RelayReachState.DONE] relay has no incompleteness frontier — it has loaded everything it has —
|
||||
* so it does NOT mark its own history bottom mid-stream (which would read like a false "incomplete below
|
||||
* here" line). Instead every done relay sinks to the **oldest-end gap** ([olderCreatedAt] null), where it
|
||||
* renders as one "fully loaded" marker. Only [REACHING][RelayReachState.REACHING] /
|
||||
* [STALLED][RelayReachState.STALLED] relays — the genuine "below here may still be incomplete" frontiers —
|
||||
* are placed at their reached cursor.
|
||||
*/
|
||||
@Composable
|
||||
fun RelayReachMarkers(
|
||||
@@ -183,7 +194,14 @@ fun RelayReachMarkers(
|
||||
) {
|
||||
val here =
|
||||
remember(limits, newerCreatedAt, olderCreatedAt) {
|
||||
limits.filter { reachedFallsInGap(it.reachedUntil, newerCreatedAt, olderCreatedAt) }
|
||||
limits.filter {
|
||||
if (it.state == RelayReachState.DONE) {
|
||||
// Fully loaded → sink to the oldest end rather than mark a frontier it doesn't have.
|
||||
newerCreatedAt != null && olderCreatedAt == null
|
||||
} else {
|
||||
reachedFallsInGap(it.reachedUntil, newerCreatedAt, olderCreatedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (here.isEmpty()) return
|
||||
|
||||
@@ -196,11 +214,16 @@ fun RelayReachMarkers(
|
||||
* down (older) in the stream — relays that race ahead leave their marker deep while slower relays'
|
||||
* markers trail higher up, converging as they catch up.
|
||||
*
|
||||
* A leading "Relay sync:" label gives the glyphs context; then each state renders one compact label:
|
||||
* a relay's host name when it is the only one of its state there (the usual converged case, where each
|
||||
* relay sits at its own depth), or just a count when several pile up at the same depth (e.g. all nine
|
||||
* clustered at the live-tail floor on first open) so the line can't grow into an unreadable comma list.
|
||||
* Reads e.g. "Relay sync: ✓ 8 · ↓ 1" or "Relay sync: ↓ nostr.wine".
|
||||
* The line is always captioned so it's never a bare glyph cluster: the live frontiers
|
||||
* ([REACHING][RelayReachState.REACHING] / [STALLED][RelayReachState.STALLED]) read "Loading:"; the
|
||||
* oldest-end pile of [DONE][RelayReachState.DONE] relays reads "Fully loaded:". Each state then renders
|
||||
* one compact label: the host name(s) when one — or two short-named — relays sit at that state (the usual
|
||||
* converged case, where each relay rests at its own depth), or just a count when several pile up at the
|
||||
* same depth (e.g. all nine clustered at the oldest-end floor) so the line can't grow into an unreadable
|
||||
* comma list. In the rare mixed line (an active frontier sharing the oldest-end gap with done relays) the
|
||||
* caption is "Loading:", so the done chip is suffixed "(fully loaded)" to keep its meaning clear. Either
|
||||
* way the whole marker is tappable for the full per-relay breakdown. Reads e.g. "Loading: ↓ nostr.wine"
|
||||
* or "Fully loaded: ✓ 8".
|
||||
*/
|
||||
@Composable
|
||||
private fun RelayReachMarker(
|
||||
@@ -209,14 +232,21 @@ private fun RelayReachMarker(
|
||||
) {
|
||||
if (entries.isEmpty()) return
|
||||
|
||||
val hasActiveFrontier = entries.any { it.state != RelayReachState.DONE }
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.padding(5.dp).then(if (onClick != null) Modifier.clickable { onClick() } else Modifier),
|
||||
) {
|
||||
HorizontalDivider(modifier = Modifier.weight(1f), thickness = DividerThickness)
|
||||
// Always caption the line so it's never a bare glyph cluster: live frontiers are "Loading:"; the
|
||||
// oldest-end pile of only-done relays is "Fully loaded:".
|
||||
Text(
|
||||
text = stringResource(Res.string.chats_history_relay_sync),
|
||||
text =
|
||||
stringResource(
|
||||
if (hasActiveFrontier) Res.string.chats_history_loading_label else Res.string.chats_history_fully_loaded_label,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
@@ -236,8 +266,21 @@ private fun RelayReachMarker(
|
||||
if (index > 0) {
|
||||
Text("·", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp)
|
||||
}
|
||||
// Spell out 1–2 short host names; otherwise a count. Done relays count as "N relays" so the
|
||||
// fully-loaded floor reads as a sentence ("✓ 8 relays"), not a bare number; active frontiers
|
||||
// stay terse ("↓ 1"). Only a mixed line (caption "Loading:") needs the done chip tagged
|
||||
// "(fully loaded)" — a pure-done line already says so in its "Fully loaded:" caption.
|
||||
val names = list.map { it.name }
|
||||
val inlineNames = reachInlineNames(names)
|
||||
val label =
|
||||
when {
|
||||
inlineNames != null -> inlineNames
|
||||
state == RelayReachState.DONE -> pluralStringResource(Res.plurals.chats_history_relays, names.size, names.size)
|
||||
else -> names.size.toString()
|
||||
}
|
||||
val chip = reachGlyph(state) + " " + label
|
||||
Text(
|
||||
text = reachGlyph(state) + " " + if (list.size == 1) list.first().name else list.size.toString(),
|
||||
text = if (state == RelayReachState.DONE && hasActiveFrontier) chip + " " + stringResource(Res.string.chats_history_fully_loaded) else chip,
|
||||
color = reachColor(state),
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
@@ -249,6 +292,21 @@ private fun RelayReachMarker(
|
||||
}
|
||||
}
|
||||
|
||||
// Host names short enough to spell out inline on the single-line divider instead of collapsing to a
|
||||
// bare count: a name up to [INLINE_NAME_MAX] when it's the lone relay of its state, or two names each
|
||||
// up to [INLINE_TWO_NAMES_MAX] when a pair shares it. Longer hosts, or 3+ relays at one state, return
|
||||
// null so the caller renders a count instead and the line can't grow unbounded — the tap-through dialog
|
||||
// always lists them all.
|
||||
private const val INLINE_NAME_MAX = 16
|
||||
private const val INLINE_TWO_NAMES_MAX = 12
|
||||
|
||||
internal fun reachInlineNames(names: List<String>): String? =
|
||||
when {
|
||||
names.size == 1 && names[0].length <= INLINE_NAME_MAX -> names[0]
|
||||
names.size == 2 && names.all { it.length <= INLINE_TWO_NAMES_MAX } -> names.joinToString(", ")
|
||||
else -> null
|
||||
}
|
||||
|
||||
internal fun reachGlyph(state: RelayReachState) =
|
||||
when (state) {
|
||||
RelayReachState.REACHING -> "↓"
|
||||
|
||||
Reference in New Issue
Block a user