docs: plan to make location (geohash) chats first-class in Amethyst

Integration plan across Home (live bubble), Messages (rooms list + the new
"+" conversation chooser), and a map-based teleport picker. Reuses the existing
kind-10081 geohash follow list as the "joined location channels" list and routes
geohash chat through LocalCache/feeds. Documents the anonymity constraint (the
"follows are here" signal comes from kind-1 geo-notes, not the anonymous chat)
and the open decisions.

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 00:21:51 +00:00
parent 0ae8316e3b
commit a1b4d33c8f
@@ -0,0 +1,153 @@
# Making location (geohash) chats first-class in Amethyst
Builds on `2026-07-15-bitchat-geohash-interop.md` (Phase 1 shipped: protocol,
geo-relay routing, a self-contained chat screen, `amy geochat`). This plan takes
it from a bolt-on screen to a native feature woven into Home, Messages, and the
map.
## The one constraint that shapes everything
Geohash **chat** (kind 20000) is signed with anonymous per-cell throwaway keys,
unlinkable to npubs. So "which of my follows are chatting here" is **not
derivable from the chat stream**. Any "follows are active near you" signal must
come from a *linkable* source:
- **kind-1 geohash notes** (`GeoHashFeedFilter` scans `LocalCache.notes` for
`isTaggedGeoHash`; authors are real npubs) → intersect with
`account.kind3FollowList` = genuine "follows near this place." This is the
template `HomeLiveFilter.followsThatParticipateOn` already uses, just sourced
from notes instead of chat events.
- **kind-10081 geohash follow lists** (`GeohashListEvent`) — the user's own, and
optionally follows' public lists.
- Anonymous **liveliness** (kind-20000/20001 presence counts) — "N people here",
no identities.
The Home bubble is therefore: *anonymous liveliness + follows' geo-note activity
→ tap into the cell's chat.* We will not imply the chat reveals who's there.
## Key architectural decision: reuse what exists
1. **Joined channels = the geohash follow list (kind 10081).** `account.geohashList`
(`model/nip51Lists/geohashLists/GeohashListState.kt`, `flow: StateFlow<Set<String>>`,
`follow`/`unfollow`, NIP-44-private capable) already models "geohashes I care
about." Treat *following a geohash* as *joining its location channel*. No new
list type, and it's private-capable. (Trade-off: today it also drives the
kind-1 notes feed; we're overloading one list for both "notes near here" and
"chat here." Acceptable — it's the same user intent. Alternative if we want
separation: a local, unpublished joined-set matching Bitchat's ephemerality.)
2. **Make geohash chat LocalCache-backed** so it can flow through the same feed
machinery as every other room. This is the crux of "first-class": the Messages
list, Home bubbles, unread counts, and pins all read from `LocalCache`
channels. Model it on the ephemeral-chat feature end to end.
## Phase A — Data model + LocalCache integration (foundation)
- `commons/.../model/geohashChat/GeohashChatChannel.kt` — a `Channel` subtype
keyed by geohash (mirror `model/emphChat/EphemeralChatChannel.kt`).
`toBestDisplayName()` returns the reverse-geocoded place (or `#geohash`).
- `LocalCache`: add a `geohashChannels` map + a consumer that routes
`GeohashChatEvent`/`GeohashPresenceEvent` into the channel (mirror
`ephemeralChannels`/`liveChatChannels` + `getOrCreateEphemeralChannel`).
- A rooms-list **subassembler** that subscribes to the joined geohashes
(`account.geohashList.flow``GeoRelayDirectory.closestRelays(g)`, kinds
20000/20001, `#g`) and feeds LocalCache — mirror
`chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt` +
`FilterFollowingEphemeralChats.kt`.
- Migrate `GeohashChatViewModel` to read the channel's notes from LocalCache
(via `LocalCache.observeNotes` like `NestLobbyScreen`) instead of its private
subscription — unifies the live view with the cached one and lets the screen
reuse the full `ChatroomMessageCompose` (reactions, replies) later. Keep the
send path (per-geohash signer + PoW + geo relays) as-is.
- Ephemeral caveat: relays don't store kind 20000, so joined-cell rooms only show
messages seen while subscribed. Scope background subscriptions to *joined* cells
(+ the current-location cell) to bound battery/relay load; document the cap.
## Phase B — Messages tab
- `chats/rooms/dal/ChatroomListKnownFeedFilter.kt` — add a 7th family
(`geohashChannels` from `account.geohashList`) to `feed()` + `applyFilter`
(newest message per cell), mirroring `filterRelevantEphemeralChats`.
- `chats/rooms/ChatroomHeaderCompose.kt` — add a `GeohashRoomCompose` branch in
`ChatroomEntry``nav.nav(Route.GeohashChat(geohash))`, with a location-pin
`HeaderPill`, the cell name (`LoadCityName`), and a live participant count (no
avatars — anonymous).
- `chats/rooms/NewConversationScreen.kt` — append one `ConversationType`
("Location chat", geohash icon/accent, pros/cons, `route = Route.NewGeohashChat`)
to the *Relay* section of `conversationSections` (single source of truth).
## Phase C — The builder ("+" → create/join)
- `Route.NewGeohashChat` + `NewGeohashChatScreen.kt` (mirror
`ephemChat/metadata/NewEphemeralChatScreen.kt`). Three ways to pick a cell:
1. **Current location levels.** New `GeohashChannelLevel` mapper
(region=2, province=4, city=5, neighborhood=6, block=7, building=8 chars —
the Bitchat levels; `GeohashPrecision` has the char counts but not the
names). From `LocationState.geohashStateFlow` (raise its hardcoded 5-char
precision to 8 so we can truncate to each level), list the six cells with
`LoadCityName` + a Join/Open button.
2. **Manual geohash** text field (validate against the base32 alphabet).
3. **Teleport** → the map picker (Phase E).
- Join = `account.geohashList.follow(geohash)` then `nav.nav(Route.GeohashChat)`.
- Reuse `LocationAsHash`/`ILocationGrabber` for the permission flow.
## Phase D — Home "live near you" bubble
- New feed state `homeGeohashLive` in `AccountFeedContentStates.kt` (parallel to
`homeLive`; different signal source, so not folded into `HomeLiveFilter`).
Sources: joined cells with recent activity (LocalCache, post Phase A) + the
current-location cell.
- `home/live/RenderGeohashBubble.kt` — a bubble showing the cell name, a
liveliness dot (reuse `LiveStatusIndicator` pattern; "online" = recent presence),
and social proof from **geo-notes**: "N follows posted near · M chatting"
(follows-near count = `GeoHashFeedFilter` authors ∩ `kind3FollowList`).
- `HomeScreen.kt` `DisplayLiveBubbles` — add the `GeohashChatChannel` (or a
synthetic geohash item) case to the type dispatch; click → `Route.GeohashChat`.
## Phase E — Teleport + map
- `LocationPickerMap.kt` — extend the display-only osmdroid `LocationPreviewMap`
with a `MapEventsOverlay`/`MapEventsReceiver` so long-press/tap drops a pin and
yields a coordinate → `GeoHash.encode(lat, lon, level)`. osmdroid (Apache-2.0)
already supports this; only the wiring is new.
- Teleport screen (or a mode in `NewGeohashChatScreen`): pick a point on the map,
show the resulting cell name + level selector, Open → `Route.GeohashChat`.
- **Auto-teleport flag:** in `GeohashChatViewModel`, compare the channel's geohash
to the current-location cell (`LocationState`); when they differ (or no
permission), pass `teleported = true` to the already-plumbed
`sendMessage(..., teleported)`. Add a manual override toggle in the composer.
- Optional: forward geocoding (place-name search) via
`Geocoder.getFromLocationName` — none exists today; small addition for a
"search a place" box.
## Phase F — Privacy, presence, polish
- **"Post as my real account" opt-in** (global or per-channel, with a
location-exposure warning). This is also the *only* way a follow becomes
visible in the chat itself — relevant to the Home social-proof story.
- **Presence heartbeats:** emit kind 20001 periodically while a channel is open
(`GeohashChatViewModel.announcePresence` already exists; schedule it).
- **Privacy note:** subscribing to a cell reveals interest in that location to
its relays (via the `#g` REQ from your IP); Tor mitigates. The kind-10081 list
can stay NIP-44-private.
- Extract hardcoded screen strings to `strings.xml` (`<plurals>` for the counts —
see `res/CLAUDE.md`); desktop `GeohashChatScreen` equivalent.
## Open decisions (need a call)
1. **Joined list:** reuse kind-10081 geohash follow list (recommended, private,
already wired) vs. a separate local ephemeral joined-set (closer to Bitchat).
2. **LocalCache integration depth:** full (Phase A — enables Messages/Home/unread,
bigger) vs. keep the self-contained screen and only add the builder + a
Home bubble fed by geo-notes (smaller, but not truly "in the Messages list").
3. **Default identity in these rooms:** stays anonymous per-cell (recommended);
the real-account opt-in is Phase F.
## Verification
- Unit: `GeohashChannelLevel` bucketing, the joined-list ↔ subscription wiring,
the geo-note follow-intersection count.
- `amy geochat` remains the wire-level interop check against a real Bitchat cell.
- Drive the app: join a cell from the "+" chooser, confirm it appears in Messages,
post/receive, teleport via the map and confirm the `["t","teleport"]` tag, and
confirm the Home bubble reflects a follow's kind-1 geo-note near you.