feat: pin followed geohash channels to the bottom nav

Adds a "Location Channels" category to the bottom-bar settings picker so a
user can pin their joined geohash cells as avatars alongside PublicChat,
RelayGroup and Concord groups. Each pinned cell renders a location-pin
robohash keyed by the geohash, labelled "#<cell>", opening the location
chat when tapped.

- BottomBarEntry.Geohash sealed variant (+ stable key)
- NavBarItem.GEOHASH_CHATS catalog entry in the chats category
- rememberGeohashEntryDisplay resolves the anonymous avatar/route (no
  metadata REQ — the cell is anonymous by design)
- picker/settings and live-bar group branches route geohash through the
  shared GroupEntryDisplay path
- no feed preloader (the cell's message subscription opens from the chat
  screen itself)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172JoMccseEKenyWan6txWV
This commit is contained in:
Claude
2026-07-16 22:11:44 +00:00
parent 067e3039ce
commit 3bd490b58c
7 changed files with 48 additions and 0 deletions
@@ -236,6 +236,7 @@ internal fun rememberBottomBarSlot(
is BottomBarEntry.PublicChat,
is BottomBarEntry.RelayGroup,
is BottomBarEntry.Concord,
is BottomBarEntry.Geohash,
-> {
val display = rememberGroupEntryDisplay(entry, accountViewModel) ?: return null
// A pinned chat/group shows its avatar, like the favorite-app tabs — icon only.
@@ -76,6 +76,13 @@ sealed interface BottomBarEntry {
data class Concord(
val communityId: String,
) : BottomBarEntry
/** A pinned Bitchat geohash location channel, keyed by its geohash cell; opens the location chat. */
@Serializable
@SerialName("geohash")
data class Geohash(
val geohash: String,
) : BottomBarEntry
}
/**
@@ -90,6 +97,7 @@ val BottomBarEntry.stableKey: String
is BottomBarEntry.PublicChat -> "publicChat:$channelId"
is BottomBarEntry.RelayGroup -> "relayGroup:$relayUrl|$groupId"
is BottomBarEntry.Concord -> "concord:$communityId"
is BottomBarEntry.Geohash -> "geohash:$geohash"
}
/** The favorite-app ids in this bottom-bar config — the apps that should be kept warm as bottom-row tabs. */
@@ -172,6 +172,21 @@ fun rememberConcordEntryDisplay(
)
}
/**
* 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.
*/
@Composable
fun rememberGeohashEntryDisplay(entry: BottomBarEntry.Geohash): GroupEntryDisplay =
remember(entry.geohash) {
GroupEntryDisplay(
label = "#${entry.geohash}",
robotSeed = entry.geohash,
model = null,
route = Route.GeohashChat(entry.geohash),
)
}
/**
* Resolves any chat/group entry to its live display, or null for a non-group entry. [subscribe] is
* forwarded to the channel observers: true (the default) keeps a relay REQ open — used by the live
@@ -187,6 +202,7 @@ fun rememberGroupEntryDisplay(
is BottomBarEntry.PublicChat -> rememberPublicChatEntryDisplay(entry, accountViewModel, subscribe)
is BottomBarEntry.RelayGroup -> rememberRelayGroupEntryDisplay(entry, accountViewModel, subscribe)
is BottomBarEntry.Concord -> rememberConcordEntryDisplay(entry, accountViewModel)
is BottomBarEntry.Geohash -> rememberGeohashEntryDisplay(entry)
is BottomBarEntry.BuiltIn -> null
is BottomBarEntry.Favorite -> null
}
@@ -68,6 +68,7 @@ enum class NavBarItem {
PUBLIC_CHATS,
RELAY_GROUPS,
CONCORD,
GEOHASH_CHATS,
FOLLOW_PACKS,
LIVE_STREAMS,
NESTS,
@@ -334,6 +335,13 @@ val NavBarCatalog: Map<NavBarItem, NavBarItemDef> =
icon = MaterialSymbols.Group,
resolveRoute = { Route.Concords },
),
NavBarItem.GEOHASH_CHATS to
NavBarItemDef(
id = NavBarItem.GEOHASH_CHATS,
labelRes = R.string.location_channels,
icon = MaterialSymbols.LocationOn,
resolveRoute = { Route.NewGeohashChat },
),
NavBarItem.FOLLOW_PACKS to
NavBarItemDef(
id = NavBarItem.FOLLOW_PACKS,
@@ -471,6 +479,7 @@ val BottomBarCategories: List<NavBarCategory> =
NavBarItem.PUBLIC_CHATS,
NavBarItem.RELAY_GROUPS,
NavBarItem.CONCORD,
NavBarItem.GEOHASH_CHATS,
),
),
NavBarCategory(
@@ -156,6 +156,10 @@ private fun PreloadFor(
// Covered by AccountFilterAssemblerSubscription (always-on).
NavBarItem.NOTIFICATIONS -> Unit
// Anonymous location channels — the pinned avatar carries no metadata, and the cell's
// message subscription opens from the chat screen itself, so there's nothing to preload.
NavBarItem.GEOHASH_CHATS -> Unit
// No relay feed to preload (loaded by the destination screen on demand).
NavBarItem.PROFILE,
NavBarItem.MY_LISTS,
@@ -107,6 +107,7 @@ private val ExpandableItems =
NavBarItem.PUBLIC_CHATS,
NavBarItem.RELAY_GROUPS,
NavBarItem.CONCORD,
NavBarItem.GEOHASH_CHATS,
)
/** Soft guidance, not a hard cap: a Material bottom bar reads best at ~5 tabs. */
@@ -588,6 +589,13 @@ private fun PickerChildren(
GroupChildList(entries, pinnedKeys, accountViewModel, onTogglePin)
}
NavBarItem.GEOHASH_CHATS -> {
val cells by accountViewModel.account.geohashList.flow
.collectAsStateWithLifecycle()
val entries = remember(cells) { cells.sorted().map { BottomBarEntry.Geohash(it) } }
GroupChildList(entries, pinnedKeys, accountViewModel, onTogglePin)
}
else -> {}
}
}
@@ -828,6 +836,7 @@ private fun rememberPinnedVisual(
is BottomBarEntry.PublicChat,
is BottomBarEntry.RelayGroup,
is BottomBarEntry.Concord,
is BottomBarEntry.Geohash,
-> {
// Read-only: the settings list resolves from cache; the live bar owns the subscription.
val display = rememberGroupEntryDisplay(entry, accountViewModel, subscribe = false)
+1
View File
@@ -962,6 +962,7 @@
<string name="nip82_older_releases_hide">Hide older releases</string>
<string name="shorts">Shorts</string>
<string name="public_chats">Public Chats</string>
<string name="location_channels">Location Channels</string>
<string name="follow_packs">Follow Packs</string>
<string name="live_streams">Live Streams</string>
<string name="nests">Nests</string>