mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat(bottom-bar): pin a whole NIP-29 relay or a single Concord channel
The bottom-bar settings picker only exposed one level of each grouped chat system: NIP-29 let you pin an individual group but not the host relay, while Concord let you pin a community but not an individual channel. Since a NIP-29 relay is the analog of a Concord community (the container) and a Concord channel is the analog of a NIP-29 group (the item), both systems now offer both levels. - Add BottomBarEntry.RelayServer(relayUrl) -> Route.RelayGroupServer (the relay's home page of all joined groups) and BottomBarEntry.ConcordChannel( communityId, channelId, relays) -> Route.Concord (a specific channel), with stable @SerialName discriminators and stableKeys. - Resolve their live avatar/label/route: the relay via its cached NIP-11 doc, the channel via the community session's folded Control Plane (community icon + channel name). - Regroup the picker by container: each relay/community is an addable "server" row with its groups/channels nested beneath it, so you can add the whole server or a single room in both systems. - Bootstrap a pinned channel's community list (importConcordCommunities and the pinned-community preloader now also read ConcordChannel entries). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012v531djZmQBxVoyCm45BNY
This commit is contained in:
+2
@@ -235,7 +235,9 @@ internal fun rememberBottomBarSlot(
|
||||
}
|
||||
is BottomBarEntry.PublicChat,
|
||||
is BottomBarEntry.RelayGroup,
|
||||
is BottomBarEntry.RelayServer,
|
||||
is BottomBarEntry.Concord,
|
||||
is BottomBarEntry.ConcordChannel,
|
||||
is BottomBarEntry.Geohash,
|
||||
-> {
|
||||
val display = rememberGroupEntryDisplay(entry, accountViewModel) ?: return null
|
||||
|
||||
+32
@@ -38,6 +38,11 @@ import kotlinx.serialization.Serializable
|
||||
* - [PublicChat], [RelayGroup] and [Concord] each pin one specific joined chat the user picked from
|
||||
* their joined list (NIP-28 channel, NIP-29 relay group, or a Concord community). The bar resolves
|
||||
* each to the chat's avatar + name from the local cache and to its chat/home route for navigation.
|
||||
* - [RelayServer] and [ConcordChannel] pin the *other* level of the two grouped chat systems: a NIP-29
|
||||
* host relay (whose home page lists every group on it) and one channel inside a Concord community.
|
||||
* A NIP-29 relay is the analog of a Concord community (the container), and a Concord channel is the
|
||||
* analog of a NIP-29 group (the item) — so together the five group entries let the user pin either
|
||||
* the whole server or a single room in both systems.
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface BottomBarEntry {
|
||||
@@ -70,6 +75,17 @@ sealed interface BottomBarEntry {
|
||||
val relayUrl: String,
|
||||
) : BottomBarEntry
|
||||
|
||||
/**
|
||||
* A pinned NIP-29 host relay ("server"), keyed by its relay url; opens the relay's home page that
|
||||
* lists every group the user has joined on it. The relay-level analog of pinning a whole [Concord]
|
||||
* community, so both grouped chat systems can pin the container as well as an individual room.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("relayServer")
|
||||
data class RelayServer(
|
||||
val relayUrl: String,
|
||||
) : BottomBarEntry
|
||||
|
||||
/**
|
||||
* A pinned Concord community, keyed by its community id; opens the community's channel list.
|
||||
*
|
||||
@@ -88,6 +104,20 @@ sealed interface BottomBarEntry {
|
||||
val relays: List<String> = emptyList(),
|
||||
) : BottomBarEntry
|
||||
|
||||
/**
|
||||
* A pinned Concord channel inside a community, keyed by the (community id, channel id) pair; opens
|
||||
* that specific channel. The channel-level analog of pinning a single [RelayGroup]. [relays] carry
|
||||
* the community's bootstrap relays (same reason as [Concord.relays]) so a pinned channel whose
|
||||
* community list we haven't cached can still be resolved.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("concordChannel")
|
||||
data class ConcordChannel(
|
||||
val communityId: String,
|
||||
val channelId: String,
|
||||
val relays: List<String> = emptyList(),
|
||||
) : BottomBarEntry
|
||||
|
||||
/** A pinned Bitchat geohash location channel, keyed by its geohash cell; opens the location chat. */
|
||||
@Serializable
|
||||
@SerialName("geohash")
|
||||
@@ -107,7 +137,9 @@ val BottomBarEntry.stableKey: String
|
||||
is BottomBarEntry.Favorite -> "favorite:$favoriteId"
|
||||
is BottomBarEntry.PublicChat -> "publicChat:$channelId"
|
||||
is BottomBarEntry.RelayGroup -> "relayGroup:$relayUrl|$groupId"
|
||||
is BottomBarEntry.RelayServer -> "relayServer:$relayUrl"
|
||||
is BottomBarEntry.Concord -> "concord:$communityId"
|
||||
is BottomBarEntry.ConcordChannel -> "concordChannel:$communityId|$channelId"
|
||||
is BottomBarEntry.Geohash -> "geohash:$geohash"
|
||||
}
|
||||
|
||||
|
||||
+61
@@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.model.ChannelState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
@@ -45,6 +46,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -132,6 +134,29 @@ fun rememberRelayGroupEntryDisplay(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A pinned NIP-29 host relay ("server"). Its name/icon come from the relay's NIP-11 document (cached,
|
||||
* shared with the browse/server rows), falling back to the display host; tapping opens the relay's
|
||||
* home page listing every joined group on it.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberRelayServerEntryDisplay(
|
||||
entry: BottomBarEntry.RelayServer,
|
||||
accountViewModel: AccountViewModel,
|
||||
): GroupEntryDisplay {
|
||||
val relay = remember(entry.relayUrl) { RelayUrlNormalizer.normalizeOrNull(entry.relayUrl) }
|
||||
val host = remember(relay, entry.relayUrl) { relay?.displayUrl() ?: entry.relayUrl }
|
||||
// NIP-11 is its own HTTP cache (not a relay REQ), so this is safe even for the read-only picker.
|
||||
val info = relay?.let { loadRelayInfo(it) }
|
||||
val name = info?.value?.name?.takeIf { it.isNotBlank() } ?: host
|
||||
return GroupEntryDisplay(
|
||||
label = name,
|
||||
robotSeed = entry.relayUrl,
|
||||
model = info?.value?.icon?.ifBlank { null },
|
||||
route = Route.RelayGroupServer(entry.relayUrl),
|
||||
)
|
||||
}
|
||||
|
||||
/** [observeChannelMetadata] tolerant of a null channel (unresolvable relay), so callers avoid an early return. */
|
||||
@Composable
|
||||
private fun observeChannelMetadataOrNull(
|
||||
@@ -169,6 +194,40 @@ fun rememberConcordEntryDisplay(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A pinned Concord channel inside a community. The label is the channel's folded name; the avatar is
|
||||
* the *community's* icon (so the tab reads as "this community's channel"), keyed by the channel id so
|
||||
* distinct channels still get distinct robohash fallbacks. Tapping opens that channel directly.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberConcordChannelEntryDisplay(
|
||||
entry: BottomBarEntry.ConcordChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
): GroupEntryDisplay {
|
||||
val account = accountViewModel.account
|
||||
val revision by account.concordSessions.revision.collectAsStateWithLifecycle()
|
||||
val session = remember(entry.communityId, revision) { account.concordSessions.sessionFor(entry.communityId) }
|
||||
val state by (session?.state ?: remember { MutableStateFlow(null) }).collectAsStateWithLifecycle()
|
||||
|
||||
val folded = state.takeIf { revision >= 0 }
|
||||
val channelName =
|
||||
folded
|
||||
?.channels
|
||||
?.get(entry.channelId)
|
||||
?.definition
|
||||
?.name
|
||||
?.ifBlank { null }
|
||||
val label = channelName ?: entry.channelId.take(8)
|
||||
val model = rememberConcordImageModel(folded?.metadata?.icon, accountViewModel)
|
||||
|
||||
return GroupEntryDisplay(
|
||||
label = label,
|
||||
robotSeed = entry.channelId,
|
||||
model = model,
|
||||
route = Route.Concord(entry.communityId, entry.channelId),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A pinned geohash location channel. Anonymous by design — no metadata REQ; the row is a location
|
||||
* pin robohash keyed by the cell, labelled with the geohash, opening the location chat.
|
||||
@@ -198,7 +257,9 @@ fun rememberGroupEntryDisplay(
|
||||
when (entry) {
|
||||
is BottomBarEntry.PublicChat -> rememberPublicChatEntryDisplay(entry, accountViewModel, subscribe)
|
||||
is BottomBarEntry.RelayGroup -> rememberRelayGroupEntryDisplay(entry, accountViewModel, subscribe)
|
||||
is BottomBarEntry.RelayServer -> rememberRelayServerEntryDisplay(entry, accountViewModel)
|
||||
is BottomBarEntry.Concord -> rememberConcordEntryDisplay(entry, accountViewModel)
|
||||
is BottomBarEntry.ConcordChannel -> rememberConcordChannelEntryDisplay(entry, accountViewModel)
|
||||
is BottomBarEntry.Geohash -> rememberGeohashEntryDisplay(entry)
|
||||
is BottomBarEntry.BuiltIn -> null
|
||||
is BottomBarEntry.Favorite -> null
|
||||
|
||||
+7
-3
@@ -685,9 +685,13 @@ class AccountViewModel(
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val pinnedRelays =
|
||||
settings.uiSettingsFlow.bottomBarItems.value
|
||||
.filterIsInstance<BottomBarEntry.Concord>()
|
||||
.flatMap { it.relays }
|
||||
.mapNotNullTo(HashSet()) { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
.flatMap {
|
||||
when (it) {
|
||||
is BottomBarEntry.Concord -> it.relays
|
||||
is BottomBarEntry.ConcordChannel -> it.relays
|
||||
else -> emptyList()
|
||||
}
|
||||
}.mapNotNullTo(HashSet()) { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
account.importConcordCommunities(pinnedRelays)
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -127,9 +127,14 @@ private fun bootstrapPinnedCommunities(accountViewModel: AccountViewModel) {
|
||||
remember(items, communities) {
|
||||
val known = communities.mapTo(HashSet()) { it.id }
|
||||
items
|
||||
.filterIsInstance<BottomBarEntry.Concord>()
|
||||
.map { it.communityId }
|
||||
.filterTo(sortedSetOf()) { it !in known }
|
||||
.mapNotNull {
|
||||
// Both a pinned community and a pinned channel need their community's list fetched.
|
||||
when (it) {
|
||||
is BottomBarEntry.Concord -> it.communityId
|
||||
is BottomBarEntry.ConcordChannel -> it.communityId
|
||||
else -> null
|
||||
}
|
||||
}.filterTo(sortedSetOf()) { it !in known }
|
||||
}
|
||||
|
||||
LaunchedEffect(missingPinned) {
|
||||
|
||||
+158
-14
@@ -99,6 +99,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry
|
||||
import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.GroupTag
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/** The chat catalog items whose picker row expands to a per-item picker (favorites / joined groups). */
|
||||
private val ExpandableItems =
|
||||
@@ -557,7 +560,7 @@ private fun PickerChildren(
|
||||
label = fav.label,
|
||||
pinned = entry.stableKey in pinnedKeys,
|
||||
onToggle = { onTogglePin(entry) },
|
||||
indent = true,
|
||||
indentLevel = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -573,20 +576,41 @@ private fun PickerChildren(
|
||||
NavBarItem.RELAY_GROUPS -> {
|
||||
val groups by accountViewModel.account.relayGroupList.liveRelayGroupList
|
||||
.collectAsStateWithLifecycle()
|
||||
val entries =
|
||||
remember(groups) {
|
||||
groups
|
||||
.sortedBy { (it.name ?: it.groupId).lowercase() }
|
||||
.map { BottomBarEntry.RelayGroup(it.groupId, it.relayUrl) }
|
||||
if (groups.isEmpty()) {
|
||||
EmptyChildHint(R.string.bottom_bar_settings_no_groups)
|
||||
} else {
|
||||
// Group joined groups by their host relay: the relay itself is addable (opens its home
|
||||
// page listing every group on it) with each individual group nested beneath it. NIP-29
|
||||
// relays are the container, like Concord communities below.
|
||||
val byRelay =
|
||||
remember(groups) {
|
||||
groups
|
||||
.groupBy { it.relayUrl }
|
||||
.toList()
|
||||
.sortedBy { it.first.lowercase() }
|
||||
}
|
||||
byRelay.forEach { (relayUrl, relayGroups) ->
|
||||
key(relayUrl) {
|
||||
RelayServerPickerGroup(relayUrl, relayGroups, pinnedKeys, accountViewModel, onTogglePin)
|
||||
}
|
||||
}
|
||||
GroupChildList(entries, pinnedKeys, accountViewModel, onTogglePin)
|
||||
}
|
||||
}
|
||||
|
||||
NavBarItem.CONCORD -> {
|
||||
val communities by accountViewModel.account.concordChannelList.liveCommunities
|
||||
.collectAsStateWithLifecycle()
|
||||
val entries = remember(communities) { communities.map { BottomBarEntry.Concord(it.id, it.relays) } }
|
||||
GroupChildList(entries, pinnedKeys, accountViewModel, onTogglePin)
|
||||
if (communities.isEmpty()) {
|
||||
EmptyChildHint(R.string.bottom_bar_settings_no_groups)
|
||||
} else {
|
||||
// Group by community: the community itself is addable (opens its channel list) with each
|
||||
// channel nested beneath it — the mirror of the relay-group layout above.
|
||||
communities.forEach { community ->
|
||||
key(community.id) {
|
||||
ConcordServerPickerGroup(community, pinnedKeys, accountViewModel, onTogglePin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavBarItem.GEOHASH_CHATS -> {
|
||||
@@ -619,7 +643,110 @@ private fun GroupChildList(
|
||||
label = display.label,
|
||||
pinned = entry.stableKey in pinnedKeys,
|
||||
onToggle = { onTogglePin(entry) },
|
||||
indent = true,
|
||||
indentLevel = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One NIP-29 host relay in the picker: the relay "server" row (level 1 — pin the whole relay, opening
|
||||
* its home page of all joined groups) followed by each individual joined group nested at level 2.
|
||||
*/
|
||||
@Composable
|
||||
private fun RelayServerPickerGroup(
|
||||
relayUrl: String,
|
||||
relayGroups: List<GroupTag>,
|
||||
pinnedKeys: Set<String>,
|
||||
accountViewModel: AccountViewModel,
|
||||
onTogglePin: (BottomBarEntry) -> Unit,
|
||||
) {
|
||||
val serverEntry = remember(relayUrl) { BottomBarEntry.RelayServer(relayUrl) }
|
||||
val serverDisplay = rememberGroupEntryDisplay(serverEntry, accountViewModel, subscribe = false)
|
||||
AvailableRow(
|
||||
leading = {
|
||||
if (serverDisplay != null) {
|
||||
GroupEntryAvatar(serverDisplay, 34.dp, accountViewModel)
|
||||
} else {
|
||||
LeadingGlyph(MaterialSymbols.Dns)
|
||||
}
|
||||
},
|
||||
label = serverDisplay?.label ?: relayUrl,
|
||||
pinned = serverEntry.stableKey in pinnedKeys,
|
||||
onToggle = { onTogglePin(serverEntry) },
|
||||
indentLevel = 1,
|
||||
)
|
||||
|
||||
val sorted = remember(relayGroups) { relayGroups.sortedBy { (it.name ?: it.groupId).lowercase() } }
|
||||
sorted.forEach { tag ->
|
||||
val entry = BottomBarEntry.RelayGroup(tag.groupId, tag.relayUrl)
|
||||
val display = rememberGroupEntryDisplay(entry, accountViewModel, subscribe = false) ?: return@forEach
|
||||
AvailableRow(
|
||||
leading = { GroupEntryAvatar(display, 30.dp, accountViewModel) },
|
||||
label = display.label,
|
||||
pinned = entry.stableKey in pinnedKeys,
|
||||
onToggle = { onTogglePin(entry) },
|
||||
indentLevel = 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One Concord community in the picker: the community "server" row (level 1 — pin the whole community,
|
||||
* opening its channel list) followed by each folded channel nested at level 2. Channels come from the
|
||||
* community session's folded Control Plane; before it folds (or if its relays are dead) the list is
|
||||
* empty and only the community itself can be pinned.
|
||||
*/
|
||||
@Composable
|
||||
private fun ConcordServerPickerGroup(
|
||||
community: ConcordCommunityListEntry,
|
||||
pinnedKeys: Set<String>,
|
||||
accountViewModel: AccountViewModel,
|
||||
onTogglePin: (BottomBarEntry) -> Unit,
|
||||
) {
|
||||
val serverEntry = remember(community.id, community.relays) { BottomBarEntry.Concord(community.id, community.relays) }
|
||||
val serverDisplay = rememberGroupEntryDisplay(serverEntry, accountViewModel, subscribe = false)
|
||||
AvailableRow(
|
||||
leading = {
|
||||
if (serverDisplay != null) {
|
||||
GroupEntryAvatar(serverDisplay, 34.dp, accountViewModel)
|
||||
} else {
|
||||
LeadingGlyph(MaterialSymbols.Group)
|
||||
}
|
||||
},
|
||||
label = serverDisplay?.label ?: community.name.ifBlank { community.id.take(8) },
|
||||
pinned = serverEntry.stableKey in pinnedKeys,
|
||||
onToggle = { onTogglePin(serverEntry) },
|
||||
indentLevel = 1,
|
||||
)
|
||||
|
||||
val account = accountViewModel.account
|
||||
val revision by account.concordSessions.revision.collectAsStateWithLifecycle()
|
||||
val session = remember(community.id, revision) { account.concordSessions.sessionFor(community.id) }
|
||||
val state by (session?.state ?: remember { MutableStateFlow(null) }).collectAsStateWithLifecycle()
|
||||
|
||||
val channels =
|
||||
remember(state) {
|
||||
state
|
||||
?.channels
|
||||
?.values
|
||||
?.toList()
|
||||
.orEmpty()
|
||||
}
|
||||
channels.forEach { channel ->
|
||||
val entry = BottomBarEntry.ConcordChannel(community.id, channel.channelIdHex, community.relays)
|
||||
val def = channel.definition
|
||||
val icon =
|
||||
when {
|
||||
def.voice -> MaterialSymbols.Mic
|
||||
def.private -> MaterialSymbols.Lock
|
||||
else -> MaterialSymbols.Tag
|
||||
}
|
||||
AvailableRow(
|
||||
leading = { LeadingGlyph(icon) },
|
||||
label = def.name.ifBlank { channel.channelIdHex.take(8) },
|
||||
pinned = entry.stableKey in pinnedKeys,
|
||||
onToggle = { onTogglePin(entry) },
|
||||
indentLevel = 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -628,20 +755,32 @@ private fun GroupChildList(
|
||||
// Rows & shared bits
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Start padding per nesting depth: 0 = a top-level catalog row, 1 = an item under an expandable
|
||||
* category (a favorite, or a relay/community "server" row), 2 = a room nested under its server (a
|
||||
* NIP-29 group under its relay, or a Concord channel under its community).
|
||||
*/
|
||||
private fun indentPadding(level: Int) =
|
||||
when (level) {
|
||||
0 -> 13.dp
|
||||
1 -> 24.dp
|
||||
else -> 40.dp
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AvailableRow(
|
||||
leading: @Composable () -> Unit,
|
||||
label: String,
|
||||
pinned: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
indent: Boolean = false,
|
||||
indentLevel: Int = 0,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(start = if (indent) 24.dp else 13.dp, end = 13.dp, top = 7.dp, bottom = 7.dp),
|
||||
.padding(start = indentPadding(indentLevel), end = 13.dp, top = 7.dp, bottom = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
@@ -773,12 +912,15 @@ private fun SectionHeader(title: String) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyChildHint(textRes: Int) {
|
||||
private fun EmptyChildHint(
|
||||
textRes: Int,
|
||||
indentLevel: Int = 1,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(textRes),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(start = 24.dp, end = 13.dp, top = 6.dp, bottom = 6.dp),
|
||||
modifier = Modifier.padding(start = indentPadding(indentLevel), end = 13.dp, top = 6.dp, bottom = 6.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -835,7 +977,9 @@ private fun rememberPinnedVisual(
|
||||
}
|
||||
is BottomBarEntry.PublicChat,
|
||||
is BottomBarEntry.RelayGroup,
|
||||
is BottomBarEntry.RelayServer,
|
||||
is BottomBarEntry.Concord,
|
||||
is BottomBarEntry.ConcordChannel,
|
||||
is BottomBarEntry.Geohash,
|
||||
-> {
|
||||
// Read-only: the settings list resolves from cache; the live bar owns the subscription.
|
||||
|
||||
+7
-1
@@ -40,7 +40,9 @@ class BottomBarEntrySerializationTest {
|
||||
BottomBarEntry.Favorite("url:https://example.com"),
|
||||
BottomBarEntry.PublicChat("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb"),
|
||||
BottomBarEntry.RelayGroup("abcd1234", "wss://groups.example.com"),
|
||||
BottomBarEntry.RelayServer("wss://groups.example.com"),
|
||||
BottomBarEntry.Concord("f".repeat(64), listOf("wss://relay.ditto.pub", "wss://community.example.com")),
|
||||
BottomBarEntry.ConcordChannel("f".repeat(64), "a".repeat(32), listOf("wss://relay.ditto.pub")),
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -50,7 +52,11 @@ class BottomBarEntrySerializationTest {
|
||||
assertTrue("expected stable discriminators, got: $json", json.contains("\"builtIn\"") && json.contains("\"favorite\""))
|
||||
assertTrue(
|
||||
"expected group discriminators, got: $json",
|
||||
json.contains("\"publicChat\"") && json.contains("\"relayGroup\"") && json.contains("\"concord\""),
|
||||
json.contains("\"publicChat\"") &&
|
||||
json.contains("\"relayGroup\"") &&
|
||||
json.contains("\"relayServer\"") &&
|
||||
json.contains("\"concord\"") &&
|
||||
json.contains("\"concordChannel\""),
|
||||
)
|
||||
assertEquals(sample, JsonMapper.fromJson<List<BottomBarEntry>>(json))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user