Live location map: shared TiledMapView + live screen + dashboard section#779
Merged
Conversation
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
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
LocationTraceCanvasso the History map, dashboard preview card, Find Device, and the new live map all share one viewport heuristic.location/map/MapProjection.kt—MapViewport/MapTileKey/fitViewport/visibleTileKeys/toPx+ constants (extracted verbatim).location/map/TiledMapView.kt— generic tiled basemap + gestures, with two overlay slots:onDrawOverlay(Canvas, for traces) andmarkerContent(composables, for avatar dots), both reading the same live viewport.LocationTraceCanvaskeeps its public signature unchanged and delegates toTiledMapView(trace/playhead/dwell drawing moved intoonDrawOverlay). 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_MS30 min /AGE_LABEL_AFTER_MS2 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).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
homebase-coreon JVM, Android, wasmJs; Konsist (hardcoded-Text) green; this PR's CI verifies iOS + Desktop.NoBeanDefinition), app authenticates and enters Compose with the new wiring.LiveLocationShareService.startper Live Relay: live GPS sharing (debug-flow build) #778's "How to activate" note).🤖 Generated with Claude Code