Skip to content

Live location map: shared TiledMapView + live screen + dashboard section#779

Merged
Seifert69 merged 12 commits into
mainfrom
live-location-map
Jun 21, 2026
Merged

Live location map: shared TiledMapView + live screen + dashboard section#779
Seifert69 merged 12 commits into
mainfrom
live-location-map

Conversation

@Seifert69

Copy link
Copy Markdown
Contributor

Summary

Builds the live-location map on top of the merged Live Relay plumbing (#778), and first streamlines the map code into a shared core (the user's explicit "first order of business").

Phase 1 — refactor (behaviour-preserving)

Extract the zoom/pan/tile/projection core out of LocationTraceCanvas so the History map, dashboard preview card, Find Device, and the new live map all share one viewport heuristic.

  • location/map/MapProjection.ktMapViewport/MapTileKey/fitViewport/visibleTileKeys/toPx + constants (extracted verbatim).
  • location/map/TiledMapView.kt — generic tiled basemap + gestures, with two overlay slots: onDrawOverlay (Canvas, for traces) and markerContent (composables, for avatar dots), both reading the same live viewport.
  • LocationTraceCanvas keeps its public signature unchanged and delegates to TiledMapView (trace/playhead/dwell drawing moved into onDrawOverlay). All existing call sites untouched.

Phase 2 — feature

A separate, zoomable live map: one avatar dot per identity in the in-memory receive store, plus a distinct "you" dot when sharing; updates as the store updates.

  • livelocation/: LiveLocationUiState (+ LIVE_STALE_MS 30 min / AGE_LABEL_AFTER_MS 2 min), LiveLocationViewModel (positions + 30 s ticker, self dot, staleness filter, avatar resolve), LiveLocationMarker (ringed avatar + "Nm" age label when >2 min), LiveLocationMap (keyed, offset-positioned markers), LiveLocationScreen.
  • Route.LocationLive + AppNavHost; viewModelOf(::LiveLocationViewModel).
  • Dashboard: "Live location sharing" section ABOVE Location History, visible when sharing OR a ≤30 min inbound point exists; links to the live map.

Dots older than 30 min drop; older than 2 min show a minute label. Chat-bubble entry deferred to the future live-share message kind (Dashboard is the v1 entry, per decision).

Verification

  • ✅ Compiles: homebase-core on JVM, Android, wasmJs; Konsist (hardcoded-Text) green; this PR's CI verifies iOS + Desktop.
  • ✅ Desktop smoke launch: Koin DI graph builds (no NoBeanDefinition), app authenticates and enters Compose with the new wiring.
  • Interactive visual check (recommended on a real device/desktop): History/Dashboard-preview/Find-Device pan-zoom-playback unchanged after the refactor; and with a second identity sharing, avatar dots render on the live map (driven via LiveLocationShareService.start per Live Relay: live GPS sharing (debug-flow build) #778's "How to activate" note).

🤖 Generated with Claude Code

Phase 1 (refactor, behaviour-preserving): extract the zoom/pan/tile/projection
core out of LocationTraceCanvas into a generic, reusable map composable so the
History map, dashboard preview, Find Device, and the new live map all share one
viewport heuristic.
- new map/MapProjection.kt: MapViewport/MapTileKey/fitViewport/visibleTileKeys/toPx
  + constants (extracted verbatim from LocationTraceCanvas privates).
- new map/TiledMapView.kt: generic tiled basemap + gestures, with two overlay
  slots — onDrawOverlay (Canvas, for traces) and markerContent (composables, for
  avatar dots), both reading the same live viewport.
- LocationTraceCanvas keeps its public signature, now delegates to TiledMapView
  with the trace/playhead/dwell drawing moved into onDrawOverlay. All existing
  call sites unchanged.

Phase 2 (feature): a separate, zoomable live-location map showing one avatar dot
per identity in the in-memory receive store (plus a distinct "you" dot when
sharing), updating as the store updates.
- livelocation/: LiveLocationUiState (+ LIVE_STALE_MS 30min / AGE_LABEL_AFTER_MS
  2min), LiveLocationViewModel (positions+ticker, self dot, staleness filter,
  avatar resolve), LiveLocationMarker (ringed avatar + "Nm" age label >2min),
  LiveLocationMap (TiledMapView markerContent, keyed/offset-positioned), LiveLocationScreen.
- Route.LocationLive + AppNavHost wiring; viewModelOf(::LiveLocationViewModel).
- Dashboard: "Live location sharing" section ABOVE Location History, visible when
  sharing or a <=30min inbound point exists; links to the live map. LocationViewModel
  computes liveSharingVisible; LocationScreen threads onNavigateToLiveMap.

Dots older than 30min drop; older than 2min show a minute label. Chat-bubble entry
deferred to the future live-share message kind. All commonMain; Konsist green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown

JVM Test Results

2 363 tests  +2   2 358 ✅ +2   2m 2s ⏱️ +10s
  287 suites ±0       5 💤 ±0 
  287 files   ±0       0 ❌ ±0 

Results for commit e90b268. ± Comparison against base commit e855acd.

♻️ This comment has been updated with latest results.

Seifert69 and others added 11 commits June 21, 2026 10:01
…ng dots

Three fixes from real-device testing:

1. Crash on "Open live map": LiveLocationViewModel was registered with
   viewModelOf, which autowires every ctor param and ignores Kotlin defaults —
   it tried to resolve the `nowMs: () -> Long` Function0 from Koin and threw
   NoDefinitionFoundException at navigation time. Register it with a manual
   viewModel { } block instead (mirrors LocationViewModel).

2. Always show your own dot when the map is open (not only while sharing),
   whenever this device has a known fix — gracefully omitted when there's no fix
   (e.g. location permission not granted / a viewer device with no GPS). Self
   position now rides pointStore.lastPoint in the combine, so it updates live.

3. De-overlap cue: markers at (near-)identical coordinates (rounded to ~1.1m)
   now fan out evenly around a small circle so each stays individually visible,
   instead of stacking invisibly. New clusterIndex/clusterSize on LiveMarker +
   fanOffset() in the marker layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
The "Live location sharing" section title is now always present on the location
dashboard. When there's live data (sharing, or an inbound point <=30min) it shows
the clickable "Open live map" card; otherwise a non-clickable "There's no live
location data at the moment." row, so the section no longer disappears entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
Phase 1 — fix the data model: a location's coordinate descriptor (lat/lon/
address) was wrongly riding on the map-PNG payload's descriptorContent. It now
lives in the message header (appData.content), exactly like Event/Poll/DiceRoll.
- MessageContent.Location(descriptor) typed kind; MessageContentParser parses
  dataType 211 from the header (falls back to null for old payload-format
  messages, which keep rendering via the media path), serializes, and reports
  usesRawHeaderContent=true.
- Send routes through the existing sendNewTypedMessage(MessageContent.Location,
  pngBundle): descriptor -> header, the map PNG stays a chat_loc payload.
- Render reuses the existing LocationPreviewCard via the media path: the chat_loc
  case reads the header descriptor (threaded from MessageBubbleRaw) and falls back
  to the payload descriptor for old messages.

Phase 2 — live location sharing on that clean structure:
- liveShareUntilMs on the descriptor -> STATIC/LIVE/ENDED in the bubble: sender
  "Share live location" + duration menu (15m/30m/1h/2h/4h) / "Stop sharing" +
  "42m left"; ENDED is final; both sides show the live caption; tap the map while
  live opens the Live Location map.
- Start/stop is now a plain updateMessage(content = new descriptor JSON) — works
  because Location is raw-header — syncing to both sides. Live-share controls only
  on new-format messages.
- Long-press fix: the location bubble uses detectTapGestures (not .clickable), so
  long-press reaches the message menu again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
…py = compat/sidecar)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
…ent sizing

From real-device testing:
1) Render location as a self-contained typed bubble in MessageBubbleRaw (like
   Event) instead of falling through to the media path. The fall-through showed
   the generic text line, which MessageMapper sets to displayLabel (= the
   address) — duplicating the address that the card caption already shows. Typed
   dispatch returns early, so no stray text line.
2) The "Share live location" / "Stop sharing" / "live · Nm left" action is now the
   last line of the caption (below the address), not above it.
3) Drop the raw lat/lon coordinate line from the bubble (and the staging card) —
   not human-readable.
4) Size the bubble like Event (widthIn 240..320, rounded container) and let the
   map image fill the width (ContentScale.Crop, fixed height) instead of
   letterboxing inside a full-width bubble.

Old payload-format location messages still render via the media path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
A typed location message has no separate text body, so the caption typed
alongside the location was dropped. Store it in LocationPreviewDescriptor.caption
(like Event's title/description), set it on send (trimmed, capped to 2000
codepoints for the header budget), render it below the address in the bubble, and
prefer it for the conversation-list preview. Preserved across live-share edits
(the descriptor is copied).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
Per design feedback: the fixed location metadata (address, the "Nm left" live
caption) is now muted grey like the Event bubble, and the user's typed caption
renders in the regular message text color BELOW the fixed parts (address +
share-live action), rather than between them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
"Everything is blue" on a sent bubble: the address used onSurfaceVariant and the
share link used primary (blue) — invisible on the blue sent-bubble background.
Thread a contentColor (bubbleSentOnSurface on sent, onSurface on received) into
LocationPreviewCard and derive the fixed/muted text from it (alpha 0.7) and the
share link + pin icon + user caption from it at full strength — so everything
stays legible on both grey and tinted bubbles (Event's approach).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
Tapping "Share live location" only edited the message's liveShareUntilMs
descriptor — the synced declaration that flips the bubble to LIVE — but never
touched the relay roster, so GPS was never armed and no position was ever sent
(the recipient's map opened empty).

Wire the bubble's start/stop to LiveLocationShareService, the existing
{identity, end-time-utc-ms} roster + relay:

- start(recipients, endTimeMs): take an ABSOLUTE end-time (not a duration) so
  the roster end-time is byte-identical to the descriptor window the VM wrote.
  That {recipient, end-time} identity is the share's key. Also relay the current
  GPS fix immediately so the server's retained last-point is fresh from second
  one (recipients are hydrated on connect without waiting for the next OS fix).
- stop(recipients, endTimeMs) + LiveShareRoster.remove(...): drop exactly this
  share's {recipient, end-time} entries — other live shares keep running. The
  old global stop() becomes stopAll(), reserved for logout/reset.
- ConversationListViewModel computes the end-time once and hands the same value
  to both updateMessage(descriptor) and liveShareService.start(); stop reads the
  share's end-time off the descriptor before overwriting it to ENDED.

Roster gains no schema change — the end-time alone discriminates shares.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
…t bubble

The location text showed blue-on-blue: the dispatch painted the card with
bubbleSentSurface for sentByYou and set contentColor = bubbleSentOnSurface,
which in dark theme is a dark-navy (OnPrimary 0xFF1E2438) — i.e. blue text on a
blue bubble. The Event bubble (the requested reference) never does this: it
renders a neutral surfaceContainerHigh card with onSurface text, identical for
sender and receiver. Match that — drop the sentByYou tint so the address is grey
(onSurface @0.7), the share affordance is legible, and the user's caption is
normal text color, on both sides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TuDsdGeN674eXJKDkCvJa
@Seifert69 Seifert69 enabled auto-merge June 21, 2026 20:13
@Seifert69 Seifert69 merged commit f0e1ba6 into main Jun 21, 2026
6 checks passed
@Seifert69 Seifert69 deleted the live-location-map branch June 21, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant