From a1b4d33c8f961a7efd7e9b3fbedf13a3d44d3181 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 00:21:51 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_0172JoMccseEKenyWan6txWV --- .../2026-07-16-location-chats-first-class.md | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 amethyst/plans/2026-07-16-location-chats-first-class.md diff --git a/amethyst/plans/2026-07-16-location-chats-first-class.md b/amethyst/plans/2026-07-16-location-chats-first-class.md new file mode 100644 index 0000000000..1b8ee50688 --- /dev/null +++ b/amethyst/plans/2026-07-16-location-chats-first-class.md @@ -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>`, + `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` (`` 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.