-
Notifications
You must be signed in to change notification settings - Fork 391
Add live location sharing #1745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alisa911
wants to merge
41
commits into
main
Choose a base branch
from
270526_1
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 9 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
15a2eb3
Add live track browser simulator
alisa911 03e15df
Add live location sharing: WebSocket tracking, map layer, context men…
alisa911 469497d
Move follow button to participant row for per-user map pan
alisa911 4a1982e
Add save to Live Tracks button
alisa911 c1bad08
Fix live track history loading
alisa911 e84b041
Add live track creation and fix owner sharing controls
alisa911 3f1e29b
Merge master
alisa911 331c18f
Add live track sharing duration parameter
alisa911 15aefff
Live track E2E encryption
alisa911 5911bc7
Fix live tracking reconnect and adapt to encrypted-only location prot…
alisa911 5ae1814
Rename live track functions and restrict live tracks page for unauthe…
alisa911 7262ddd
Add live track history loading by time interval and load-earlier button
alisa911 84f58f1
Add live track link regeneration with old-link revocation
alisa911 3dd0854
Show extra live track fields from mobile broadcaster
alisa911 386fa59
Add request/approve sharing UI for live tracks
alisa911 13208dd
Use translations for live track count and time-ago labels
alisa911 d9bba38
Remove unused variable in live track simulator
alisa911 b35f70e
Redirect live tracks login from effect instead of during render
alisa911 1b87ed5
Use route constants for live track share URL
alisa911 0d3e32e
Show create error in live track dialog instead of hanging
alisa911 cc7843d
Re-subscribe live tracks after websocket reconnect
alisa911 719171e
Fix translationId description in live track crypto header
alisa911 86d2206
Handle clipboard write failure in live track create dialog
alisa911 80aca3c
Escape live track nickname in map tooltip to prevent XSS
alisa911 d0645d5
Use shared toHHMMSS formatter for live track durations
alisa911 8860fd5
Fix live track geolocation errors not reaching the UI
alisa911 4f0dfe5
Fix removed live tracks still receiving STOMP updates
alisa911 a8f72a7
Fix wrong Resume label on non-broadcasting live tracks
alisa911 bf7e337
Memoize live participant card stats to avoid recompute on every render
alisa911 f8928f2
Show GPS accuracy and HDOP in live track participant card
alisa911 e6b07e5
Extract live track zone logic and harden context menu UX
alisa911 db1c6b9
Use unit settings for live track card values instead of hardcoded km/m
alisa911 b7877ea
Move live track state into LiveTrackingContext and gate its WebSocket
alisa911 f1af2e4
Move LIVE_TRACKS_STORAGE_KEY out of AppContext into liveTrackUtils
alisa911 cff3c56
Formatting
alisa911 fd5f5ca
Deduplicate live track URL params and helpers
alisa911 c42fb85
Broadcast location into multiple live translations
alisa911 5f01f3d
Use apiPost wrapper for live track point requests
alisa911 7f5920d
Optimize live track updates for large histories
alisa911 86995b8
Handle chunked live track history load
alisa911 8d6baa7
Remove permanent live track duration, cap at 24h
alisa911 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import { useContext, useEffect, useRef, useState } from 'react'; | ||
| import { useMap } from 'react-leaflet'; | ||
| import L from 'leaflet'; | ||
| import AppContext from '../../context/AppContext'; | ||
| import { panToVisibleCenter } from './MapStateLayer'; | ||
|
|
||
| export default function LiveTrackLayer() { | ||
| const ctx = useContext(AppContext); | ||
| const map = useMap(); | ||
|
|
||
| // { [translationId]: { [nickname]: { polyline, marker } } } | ||
| const layersRef = useRef({}); | ||
| const [pannedFor, setPannedFor] = useState(null); | ||
|
|
||
| useEffect(() => { | ||
| const selectedTid = ctx.selectedLiveTranslation?.id ?? null; | ||
|
|
||
| // Remove layers for any translation that is not currently selected | ||
| Object.keys(layersRef.current).forEach((tid) => { | ||
| if (tid !== selectedTid) removeTidLayers(map, layersRef, tid); | ||
| }); | ||
|
|
||
| if (!selectedTid) return; | ||
|
|
||
| const byNickname = ctx.liveParticipants?.[selectedTid]; | ||
| if (!byNickname) return; | ||
|
|
||
| if (!layersRef.current[selectedTid]) layersRef.current[selectedTid] = {}; | ||
|
|
||
| const activeNicks = new Set(Object.keys(byNickname)); | ||
|
|
||
| // Remove layers for participants no longer present in selected translation | ||
| Object.keys(layersRef.current[selectedTid]).forEach((nick) => { | ||
| if (!activeNicks.has(nick)) { | ||
| const { polyline, marker } = layersRef.current[selectedTid][nick]; | ||
| if (polyline) map.removeLayer(polyline); | ||
| if (marker) map.removeLayer(marker); | ||
| delete layersRef.current[selectedTid][nick]; | ||
| } | ||
| }); | ||
|
|
||
| Object.values(byNickname).forEach((participant) => { | ||
| const { nickname, color, locations } = participant; | ||
| if (!locations || locations.length === 0) return; | ||
|
|
||
| const latLngs = locations | ||
| .slice() | ||
| .reverse() | ||
| .map((l) => [l.lat, l.lon]); | ||
| const lastLoc = locations[0]; | ||
|
|
||
| const existing = layersRef.current[selectedTid][nickname]; | ||
|
|
||
| if (existing) { | ||
| existing.polyline.setLatLngs(latLngs); | ||
| existing.marker.setLatLng([lastLoc.lat, lastLoc.lon]); | ||
| } else { | ||
| const polyline = L.polyline(latLngs, { color, weight: 4, opacity: 0.85 }).addTo(map); | ||
| const iconHtml = `<div style="width:14px;height:14px;border-radius:50%;background:${color};border:2px solid #fff;box-shadow:0 0 4px rgba(0,0,0,.5)"></div>`; | ||
| const icon = L.divIcon({ html: iconHtml, className: '', iconSize: [14, 14], iconAnchor: [7, 7] }); | ||
| const marker = L.marker([lastLoc.lat, lastLoc.lon], { icon }).addTo(map); | ||
| marker.bindTooltip(nickname, { permanent: false, direction: 'top', offset: [0, -10] }); | ||
|
alisa911 marked this conversation as resolved.
Outdated
|
||
| layersRef.current[selectedTid][nickname] = { polyline, marker }; | ||
|
alisa911 marked this conversation as resolved.
Outdated
|
||
| } | ||
| }); | ||
| }, [ctx.liveParticipants, ctx.selectedLiveTranslation]); | ||
|
|
||
| // Center map when a translation is selected (if data already loaded) | ||
| useEffect(() => { | ||
| const translation = ctx.selectedLiveTranslation; | ||
| if (!translation) { | ||
| setPannedFor(null); | ||
| return; | ||
| } | ||
| if (pannedFor === translation.id) return; | ||
| const panned = panToTranslation(map, ctx.liveParticipants, translation.id, ctx.infoBlockWidth); | ||
| if (panned) setPannedFor(translation.id); | ||
| }, [ctx.selectedLiveTranslation]); | ||
|
|
||
| // Center map when data arrives for the selected translation (if not panned yet) | ||
| useEffect(() => { | ||
| const translation = ctx.selectedLiveTranslation; | ||
| if (!translation) return; | ||
| if (pannedFor === translation.id) return; | ||
| const panned = panToTranslation(map, ctx.liveParticipants, translation.id, ctx.infoBlockWidth); | ||
| if (panned) setPannedFor(translation.id); | ||
| }, [ctx.liveParticipants]); | ||
|
|
||
| // Pan to location when Follow button is clicked in context menu. | ||
| useEffect(() => { | ||
| if (!ctx.followLiveLocation) return; | ||
| const infoBlockWidthPx = Number.parseInt(String(ctx.infoBlockWidth), 10); | ||
| panToVisibleCenter(map, ctx.followLiveLocation, infoBlockWidthPx); | ||
| ctx.setFollowLiveLocation(null); | ||
| }, [ctx.followLiveLocation]); | ||
|
|
||
| // Cleanup on unmount | ||
| useEffect(() => { | ||
| return () => removeAllLayers(map, layersRef); | ||
| }, []); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| function removeTidLayers(map, layersRef, tid) { | ||
| if (!layersRef.current[tid]) return; | ||
| Object.values(layersRef.current[tid]).forEach(({ polyline, marker }) => { | ||
| if (polyline) map.removeLayer(polyline); | ||
| if (marker) map.removeLayer(marker); | ||
| }); | ||
| delete layersRef.current[tid]; | ||
| } | ||
|
|
||
| function removeAllLayers(map, layersRef) { | ||
| Object.keys(layersRef.current).forEach((tid) => removeTidLayers(map, layersRef, tid)); | ||
| } | ||
|
|
||
| function panToTranslation(map, liveParticipants, translationId, infoBlockWidth) { | ||
| const participants = liveParticipants?.[translationId]; | ||
| if (!participants) return false; | ||
|
|
||
| const locs = Object.values(participants) | ||
| .map((p) => p.locations?.[0]) | ||
| .filter(Boolean); | ||
|
|
||
| if (locs.length === 0) return false; | ||
|
|
||
| const infoBlockWidthPx = Number.parseInt(String(infoBlockWidth), 10); | ||
| if (locs.length === 1) { | ||
| panToVisibleCenter(map, locs[0], infoBlockWidthPx); | ||
| } else { | ||
| const bounds = L.latLngBounds(locs.map((l) => [l.lat, l.lon])); | ||
| map.fitBounds(bounds, { padding: [40, 40] }); | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
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
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.