Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions map/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import GlobalFrame from './frame/GlobalFrame';
import { AppContextProvider } from './context/AppContext';
import { WeatherContextProvider } from './context/WeatherContext';
import { MapContextProvider } from './context/MapContext';
import { LiveTrackingProvider } from './context/LiveTrackingContext';
import DeleteAccountDialog from './login/dialogs/DeleteAccountDialog';
import { AppServices } from './services/AppServices';
import './variables.css';
Expand All @@ -20,6 +21,7 @@ import {
PLANROUTE_URL,
SETTINGS_URL,
TRACKS_URL,
LIVE_TRACKS_URL,
VISIBLE_TRACKS_URL,
WEATHER_URL,
EXPLORE_URL,
Expand Down Expand Up @@ -114,11 +116,11 @@ const App = () => {
{
path: '/',
element: (
<>
<LiveTrackingProvider>
<AppServices />
<NavigateGlobal />
<Outlet />
</>
</LiveTrackingProvider>
),
children: [
{
Expand Down Expand Up @@ -167,6 +169,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.
1 change: 1 addition & 0 deletions map/src/context/AppContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export const AppContextProvider = (props) => {
const [fitBoundsShareTracks, setFitBoundsShareTracks] = useState(null);

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

// selected track
const [selectedGpxFile, setSelectedGpxFile] = useState({});
const [unverifiedGpxFile, setUnverifiedGpxFile] = useState(null); // see Effect in LocalClientTrackLayer
Expand Down
109 changes: 109 additions & 0 deletions map/src/context/LiveTrackingContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useLocation, useSearchParams } from 'react-router-dom';
import { LIVE_TRACKS_URL, MAIN_URL_WITH_SLASH } from '../manager/GlobalManager';
import {
KEY_HEX_RE,
LIVE_TRACK_KEY_SESSION,
LIVE_TRACKS_STORAGE_KEY,
NAME_PARAM,
TID_PARAM,
} from '../util/livetracks/liveTrackUtils';
import useLiveTracking from '../util/hooks/live/useLiveTracking';

const LiveTrackingContext = React.createContext();

export const LiveTrackingProvider = ({ children }) => {
const location = useLocation();
const [searchParams] = useSearchParams();

const [liveTranslations, setLiveTranslations] = useState(() => {
try {
return JSON.parse(localStorage.getItem(LIVE_TRACKS_STORAGE_KEY)) ?? [];
} catch {
return [];
}
});
const [liveParticipants, setLiveParticipants] = useState({});
const [liveViewers, setLiveViewers] = useState({});
// Pending share-permission requests shown to the owner as map notifications.
const [liveShareRequests, setLiveShareRequests] = useState([]); // [{ translationId, userId, nickname }]
// approve/deny callbacks published over websocket; wired by useLiveTracking.
const [liveShareActions, setLiveShareActions] = useState(null);
const [selectedLiveTranslation, setSelectedLiveTranslation] = useState(null);
const [followLiveLocation, setFollowLiveLocation] = useState(null);
const [myBroadcastTids, setMyBroadcastTids] = useState([]);

const livePath = MAIN_URL_WITH_SLASH + LIVE_TRACKS_URL;
const openLiveTracks = location.pathname.startsWith(livePath);

// Sync the selected translation from the URL (?tid=...#<key>), restoring the AES key the map
// stashed in sessionStorage before leaflet-hash cleared the fragment.
Comment on lines +39 to +40

@RZR-UA RZR-UA Jun 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think about implementing a URL scheme with a hash saved in the query string. For example, /map/live/?hash=17/50/40&tid=xxx#key may store/clear the key and finally restore the leaflet hash to keep saved map zoom/lat/lon.

useEffect(() => {
if (!openLiveTracks) {
setSelectedLiveTranslation(null);
return;
}
const tid = searchParams.get(TID_PARAM);
if (!tid) {
setSelectedLiveTranslation(null);
return;
}
let key = null;
try {
const saved = sessionStorage.getItem(LIVE_TRACK_KEY_SESSION);
if (saved && KEY_HEX_RE.test(saved)) {
key = saved;
sessionStorage.removeItem(LIVE_TRACK_KEY_SESSION);
}
} catch {}

const fromList = liveTranslations.find((t) => t.id === tid);
if (fromList) {
const entry = key && !fromList.key ? { ...fromList, key } : fromList;
setSelectedLiveTranslation(entry);
} else {
const name = searchParams.get(NAME_PARAM) ?? '';
setSelectedLiveTranslation({ id: tid, name, ...(key ? { key } : {}) });
}
}, [openLiveTracks, location.search]);

const liveState = useMemo(
() => ({
liveTranslations,
setLiveTranslations,
liveParticipants,
setLiveParticipants,
liveViewers,
setLiveViewers,
liveShareRequests,
setLiveShareRequests,
liveShareActions,
setLiveShareActions,
selectedLiveTranslation,
setSelectedLiveTranslation,
followLiveLocation,
setFollowLiveLocation,
myBroadcastTids,
setMyBroadcastTids,
}),
[
liveTranslations,
liveParticipants,
liveViewers,
liveShareRequests,
liveShareActions,
selectedLiveTranslation,
followLiveLocation,
myBroadcastTids,
]
);

// Hold the WebSocket only when live tracks are in use: viewing the page, broadcasting, or having bookmarked translations
const enabled = openLiveTracks || myBroadcastTids.length > 0 || liveTranslations.length > 0;
const api = useLiveTracking(liveState, enabled);
const value = useMemo(() => ({ ...liveState, ...api, openLiveTracks }), [liveState, api, openLiveTracks]);

return <LiveTrackingContext.Provider value={value}>{children}</LiveTrackingContext.Provider>;
};

export default LiveTrackingContext;
2 changes: 2 additions & 0 deletions map/src/frame/GlobalFrame.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '../manager/GlobalManager';
import { useWindowSize } from '../util/hooks/useWindowSize';
import GlobalAlert from './components/GlobalAlert';
import LiveShareRequests from './components/LiveShareRequests';
import DialogTitle from '@mui/material/DialogTitle';
import dialogStyles from '../dialogs/dialog.module.css';
import DialogContent from '@mui/material/DialogContent';
Expand Down Expand Up @@ -418,6 +419,7 @@ const GlobalFrame = () => {
<OsmAndMap mainMenuWidth={MAIN_MENU_MIN_SIZE + 'px'} menuInfoWidth={MENU_INFO_SIZE} />
{ctx.globalGraph?.show && <GlobalGraph type={ctx.globalGraph.type} />}
<GlobalAlert width={width} />
<LiveShareRequests />
<Snackbar
open={!!ctx.notification}
autoHideDuration={3000}
Expand Down
61 changes: 61 additions & 0 deletions map/src/frame/components/LiveShareRequests.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useContext } from 'react';
import { Alert, IconButton, Tooltip } from '@mui/material';
import { useTranslation } from 'react-i18next';
import LiveTrackingContext from '../../context/LiveTrackingContext';
import { ReactComponent as DoneIcon } from '../../assets/icons/ic_action_done.svg';
import { ReactComponent as CloseIcon } from '../../assets/icons/ic_action_close.svg';
import styles from './liveShareRequests.module.css';

// Map notifications shown to a live-track owner when viewers ask to share their location.
// ✓ approves, ✗ (dismiss) denies. Pending requests are restored from the server on reload,
// so they reappear until the owner reacts.
export default function LiveShareRequests() {
const lttx = useContext(LiveTrackingContext);
const { t } = useTranslation();

const requests = lttx.liveShareRequests;
if (!requests?.length) {
return null;
}

const trackName = (translationId) =>
lttx.liveTranslations?.find((tr) => tr.id === translationId)?.name ?? translationId;

return (
<div className={styles.container}>
{requests.map((req) => (
<Alert
key={`${req.translationId}-${req.userId}`}
severity="info"
action={
<>
<Tooltip title={t('shared_string_apply')} arrow>
<IconButton
color="inherit"
size="small"
onClick={() => lttx.liveShareActions?.approve(req.translationId, req.userId)}
>
<DoneIcon className={styles.approveIcon} />
</IconButton>
</Tooltip>
<Tooltip title={t('shared_string_close')} arrow>
<IconButton
color="inherit"
size="small"
onClick={() => lttx.liveShareActions?.deny(req.translationId, req.userId)}
>
<CloseIcon className={styles.denyIcon} />
</IconButton>
</Tooltip>
</>
}
>
{t('web:live_track_share_request', {
nickname: req.nickname,
track: trackName(req.translationId),
})}
</Alert>
))}
</div>
);
}
19 changes: 19 additions & 0 deletions map/src/frame/components/liveShareRequests.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.container {
position: fixed;
top: 68px; /* HEADER_SIZE (60) + 8 */
right: 16px;
z-index: 1400;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 360px;
}
.approveIcon {
width: 18px;
height: 18px;
}
.denyIcon {
width: 18px;
height: 18px;
fill: #ff595e !important;
}
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
Loading