Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
15a2eb3
Add live track browser simulator
alisa911 May 27, 2026
03e15df
Add live location sharing: WebSocket tracking, map layer, context men…
alisa911 May 28, 2026
469497d
Move follow button to participant row for per-user map pan
alisa911 May 28, 2026
4a1982e
Add save to Live Tracks button
alisa911 May 29, 2026
c1bad08
Fix live track history loading
alisa911 May 29, 2026
e84b041
Add live track creation and fix owner sharing controls
alisa911 Jun 2, 2026
3f1e29b
Merge master
alisa911 Jun 3, 2026
331c18f
Add live track sharing duration parameter
alisa911 Jun 3, 2026
15aefff
Live track E2E encryption
alisa911 Jun 3, 2026
5911bc7
Fix live tracking reconnect and adapt to encrypted-only location prot…
alisa911 Jun 4, 2026
5ae1814
Rename live track functions and restrict live tracks page for unauthe…
alisa911 Jun 5, 2026
7262ddd
Add live track history loading by time interval and load-earlier button
alisa911 Jun 5, 2026
84f58f1
Add live track link regeneration with old-link revocation
alisa911 Jun 5, 2026
3dd0854
Show extra live track fields from mobile broadcaster
alisa911 Jun 5, 2026
386fa59
Add request/approve sharing UI for live tracks
alisa911 Jun 8, 2026
13208dd
Use translations for live track count and time-ago labels
alisa911 Jun 8, 2026
d9bba38
Remove unused variable in live track simulator
alisa911 Jun 8, 2026
b35f70e
Redirect live tracks login from effect instead of during render
alisa911 Jun 8, 2026
1b87ed5
Use route constants for live track share URL
alisa911 Jun 8, 2026
0d3e32e
Show create error in live track dialog instead of hanging
alisa911 Jun 9, 2026
cc7843d
Re-subscribe live tracks after websocket reconnect
alisa911 Jun 9, 2026
719171e
Fix translationId description in live track crypto header
alisa911 Jun 9, 2026
86d2206
Handle clipboard write failure in live track create dialog
alisa911 Jun 9, 2026
80aca3c
Escape live track nickname in map tooltip to prevent XSS
alisa911 Jun 9, 2026
d0645d5
Use shared toHHMMSS formatter for live track durations
alisa911 Jun 9, 2026
8860fd5
Fix live track geolocation errors not reaching the UI
alisa911 Jun 9, 2026
4f0dfe5
Fix removed live tracks still receiving STOMP updates
alisa911 Jun 9, 2026
a8f72a7
Fix wrong Resume label on non-broadcasting live tracks
alisa911 Jun 9, 2026
bf7e337
Memoize live participant card stats to avoid recompute on every render
alisa911 Jun 9, 2026
f8928f2
Show GPS accuracy and HDOP in live track participant card
alisa911 Jun 9, 2026
e6b07e5
Extract live track zone logic and harden context menu UX
alisa911 Jun 9, 2026
db1c6b9
Use unit settings for live track card values instead of hardcoded km/m
alisa911 Jun 9, 2026
b7877ea
Move live track state into LiveTrackingContext and gate its WebSocket
alisa911 Jun 9, 2026
f1af2e4
Move LIVE_TRACKS_STORAGE_KEY out of AppContext into liveTrackUtils
alisa911 Jun 9, 2026
cff3c56
Formatting
alisa911 Jun 9, 2026
fd5f5ca
Deduplicate live track URL params and helpers
alisa911 Jun 9, 2026
c42fb85
Broadcast location into multiple live translations
alisa911 Jun 10, 2026
5f01f3d
Use apiPost wrapper for live track point requests
alisa911 Jun 10, 2026
7f5920d
Optimize live track updates for large histories
alisa911 Jun 10, 2026
86995b8
Handle chunked live track history load
alisa911 Jun 10, 2026
8d6baa7
Remove permanent live track duration, cap at 24h
alisa911 Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions map/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ REACT_APP_WEBSITE_NAME='Welcome to Local OsmAnd.'
; REACT_APP_OSM_GPX_URL='https://test.osmand.net'
REACT_APP_MAX_APPROXIMATE_KM='100'
REACT_APP_DEVEL_FEATURES='yes'
REACT_APP_WS_URL='ws://localhost:8080/osmand-websocket'
Comment thread
alisa911 marked this conversation as resolved.
1 change: 1 addition & 0 deletions map/.env.production
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ REACT_APP_GPX_API='https://maptile.osmand.net'
REACT_APP_OSM_GPX_URL='https://test.osmand.net'
REACT_APP_MAX_APPROXIMATE_KM='100'
REACT_APP_DEVEL_FEATURES='no'
REACT_APP_WS_URL='wss://osmand.net/osmand-websocket'
1 change: 1 addition & 0 deletions map/.env.staging
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ REACT_APP_USER_API_SITE=
REACT_APP_GPX_API=
REACT_APP_MAX_APPROXIMATE_KM='100'
REACT_APP_DEVEL_FEATURES='yes'
REACT_APP_WS_URL='wss://test.osmand.net/osmand-websocket'
3 changes: 2 additions & 1 deletion map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@hello-pangea/dnd": "^15.0.0",
"@mui/icons-material": "^5.8.3",
"@mui/material": "^5.8.3",
"@stomp/stompjs": "^7.3.0",
"@tiptap/pm": "^3.22.4",
"@tiptap/react": "^3.22.4",
"@tiptap/starter-kit": "^3.22.4",
Expand Down Expand Up @@ -57,7 +58,7 @@
"scripts": {
"start": "yarn generate-resources && react-scripts start",
"start:mobile": "yarn generate-resources && HOST=0.0.0.0 react-scripts start",
"start:local": "yarn generate-resources && USE_LOCAL_API=yes react-scripts start",
"start:local": "yarn generate-resources && USE_LOCAL_API=yes REACT_APP_WS_URL=ws://localhost:8080/osmand-websocket react-scripts start",
"start:fallback": "yarn generate-resources && USE_MAIN_API=yes react-scripts start",
Comment thread
alisa911 marked this conversation as resolved.
"build": "yarn generate-resources && react-scripts build",
"build:staging": "yarn generate-resources && env-cmd -f .env.staging react-scripts build",
Expand Down
2 changes: 2 additions & 0 deletions map/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
PLANROUTE_URL,
SETTINGS_URL,
TRACKS_URL,
LIVE_TRACKS_URL,
VISIBLE_TRACKS_URL,
WEATHER_URL,
EXPLORE_URL,
Expand Down Expand Up @@ -167,6 +168,7 @@ const App = () => {
},
],
},
{ path: LIVE_TRACKS_URL, element: <TracksMenu /> },
{ path: VISIBLE_TRACKS_URL, element: <VisibleTracks /> },
{
path: FAVORITES_URL,
Expand Down
3 changes: 3 additions & 0 deletions map/src/assets/icons/ic_action_folder_location.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions map/src/context/AppContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export const PREVIOUS_ROUTE_STORAGE_KEY = 'previousRoute';
export const OBJECT_TYPE_TRAVEL = 'travel';
export const OBJECT_TYPE_SHARE_FILE = 'share_file';

export const LIVE_TRACKS_STORAGE_KEY = 'liveTranslations';

export const MAX_RECENT_OBJS = 5;

export const defaultConfigureMapStateValues = {
Expand Down Expand Up @@ -187,6 +189,21 @@ export const AppContextProvider = (props) => {
const [fitBoundsShareTracks, setFitBoundsShareTracks] = useState(null);

const [smartFoldersCache, setSmartFoldersCache] = useState(null);

// live tracks
const [liveTranslations, setLiveTranslations] = useState(() => {
try {
return JSON.parse(localStorage.getItem(LIVE_TRACKS_STORAGE_KEY)) ?? [];
} catch {
return [];
}
});
const [liveParticipants, setLiveParticipants] = useState({});
const [liveViewers, setLiveViewers] = useState({});
const [selectedLiveTranslation, setSelectedLiveTranslation] = useState(null);
const [followLiveLocation, setFollowLiveLocation] = useState(null);
const [myBroadcastTid, setMyBroadcastTid] = useState(null);
const [isMyBroadcastPaused, setIsMyBroadcastPaused] = useState(false);
// selected track
const [selectedGpxFile, setSelectedGpxFile] = useState({});
const [unverifiedGpxFile, setUnverifiedGpxFile] = useState(null); // see Effect in LocalClientTrackLayer
Expand Down Expand Up @@ -739,6 +756,20 @@ export const AppContextProvider = (props) => {
setStopByUrl,
closeMapObj,
setCloseMapObj,
liveTranslations,
setLiveTranslations,
liveParticipants,
setLiveParticipants,
liveViewers,
setLiveViewers,
selectedLiveTranslation,
setSelectedLiveTranslation,
followLiveLocation,
setFollowLiveLocation,
myBroadcastTid,
setMyBroadcastTid,
isMyBroadcastPaused,
setIsMyBroadcastPaused,
saveTrackToCloud,
setSaveTrackToCloud,
selectedLocalTrackObj,
Expand Down
7 changes: 4 additions & 3 deletions map/src/frame/components/titles/SubTitleMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { MenuItem, Typography } from '@mui/material';
import React from 'react';
import styles from './titles.module.css';

export default function SubTitleMenu({ text }) {
export default function SubTitleMenu({ text, rightContent }) {
return (
<MenuItem sx={{ pointerEvents: 'none' }} className={styles.subtitleItem}>
<Typography className={styles.subtitle} noWrap>
<MenuItem sx={{ pointerEvents: rightContent ? 'auto' : 'none' }} className={styles.subtitleItem}>
<Typography className={styles.subtitle} noWrap sx={{ flex: 1 }}>
{text}
</Typography>
{rightContent}
</MenuItem>
);
}
6 changes: 6 additions & 0 deletions map/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@ root.render(<App />);
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

if (process.env.NODE_ENV === 'development') {
import('./test/liveTrackSimulator').then((sim) => {
globalThis.__liveTrackSim = sim;
});
}
1 change: 1 addition & 0 deletions map/src/manager/GlobalManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const WEATHER_URL = 'weather/';
export const WEATHER_FORECAST_URL = 'forecast/';

export const TRACKS_URL = 'mydata/tracks/';
export const LIVE_TRACKS_URL = 'live/';
export const VISIBLE_TRACKS_URL = 'visible-tracks/';

export const FAVORITES_URL = 'mydata/favorites/';
Expand Down
9 changes: 8 additions & 1 deletion map/src/map/OsmAndMap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ import HeightmapLayer from './layers/HeightmapLayer';
import TravelLayer from './layers/TravelLayer';
import ShareFileLayer from './layers/ShareFileLayer';
import TrackAnalyzerLayer from './layers/TrackAnalyzerLayer';
import LiveTrackLayer from './layers/LiveTrackLayer';
import { Box } from '@mui/material';
import TransportStopsLayer from './layers/TransportStopsLayer';
import { extractAndSaveLiveTrackKey } from '../util/livetracks/liveTrackUtils';

function getInitialViewFromHash() {
const hash = window.location.hash;
if (!hash || hash.length < 2) return null;
const [zoomStr, latStr, lngStr] = hash.slice(1).split('/');
const raw = hash.slice(1);

if (extractAndSaveLiveTrackKey(raw)) return null;

const [zoomStr, latStr, lngStr] = raw.split('/');
const zoom = Number.parseInt(zoomStr, 10);
const lat = Number.parseFloat(latStr);
const lng = Number.parseFloat(lngStr);
Expand Down Expand Up @@ -224,6 +230,7 @@ const OsmAndMap = ({ mainMenuWidth, menuInfoWidth }) => {
{routersReady && <NavigationLayer geocodingData={geocodingData} region={regionData} />}
<TrackAnalyzerLayer />
<ShareFileLayer />
<LiveTrackLayer />
<TravelLayer />
<FavoriteLayer />
<WeatherLayer />
Expand Down
137 changes: 137 additions & 0 deletions map/src/map/layers/LiveTrackLayer.js
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] });
Comment thread
alisa911 marked this conversation as resolved.
Outdated
layersRef.current[selectedTid][nickname] = { polyline, marker };
Comment thread
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;
}
14 changes: 11 additions & 3 deletions map/src/menu/MainMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
PLANROUTE_URL,
SETTINGS_URL,
TRACKS_URL,
LIVE_TRACKS_URL,
VISIBLE_TRACKS_URL,
WEATHER_URL,
TRAVEL_URL,
Expand Down Expand Up @@ -371,8 +372,14 @@ export default function MainMenu({
}
}, [ctx.selectedSort]);

function matchItemByUrl(pathname) {
return items.find(
(item) => pathname.startsWith(item.url) || item.otherUrls?.some((u) => pathname.startsWith(u))
);
}

function selectMenuByUrl() {
const item = items.find((item) => location.pathname.startsWith(item.url));
const item = matchItemByUrl(location.pathname);
if (item) {
ctx.setInfoBlockWidth(MENU_INFO_OPEN_SIZE + 'px');
return selectMenu({ item, openFromUrl: true });
Expand All @@ -390,7 +397,7 @@ export default function MainMenu({
if (ctx.selectedSearchObj) {
return;
}
const matchedItem = items.find((item) => location.pathname.startsWith(item.url));
const matchedItem = matchItemByUrl(location.pathname);
if (matchedItem && !isSelectedMenuItem(matchedItem)) {
setMenuInfo(matchedItem.component);
setSelectedType(matchedItem.type);
Expand Down Expand Up @@ -446,6 +453,7 @@ export default function MainMenu({
show: true,
id: MENU_IDS.tracks,
url: MAIN_URL_WITH_SLASH + TRACKS_URL,
otherUrls: [MAIN_URL_WITH_SLASH + LIVE_TRACKS_URL],
},
{
name: t('shared_string_my_favorites'),
Expand Down Expand Up @@ -886,7 +894,7 @@ export default function MainMenu({

function navigateToUrl({ menu = null, isMain = false, params = null }) {
if (menu) {
const isSubroute = location.pathname.startsWith(menu.url) && location.pathname !== menu.url;
const isSubroute = matchItemByUrl(location.pathname) === menu && location.pathname !== menu.url;
if (isSubroute) {
return;
}
Expand Down
Loading