Skip to content

Commit 52a67cf

Browse files
authored
Merge pull request #162 from sillsdev/TT-6959-HeadStatus-component
TT-6959 Refactor AppHead component and introduce HeadStatus for improved status management
2 parents 26ef325 + 86e3a2f commit 52a67cf

File tree

3 files changed

+294
-232
lines changed

3 files changed

+294
-232
lines changed

src/renderer/src/components/App/AppHead.tsx

Lines changed: 23 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
22
import { useGetGlobal, useGlobal } from '../../context/useGlobal';
33
import { useLocation, useParams } from 'react-router-dom';
4-
import {
5-
IState,
6-
IMainStrings,
7-
IViewModeStrings,
8-
ISharedStrings,
9-
OfflineProject,
10-
} from '../../model';
4+
import { IState, IViewModeStrings } from '../../model';
115
import { shallowEqual, useSelector } from 'react-redux';
126
import {
137
AppBar,
@@ -17,14 +11,11 @@ import {
1711
LinearProgress,
1812
Tooltip,
1913
Box,
20-
Button,
2114
useTheme,
2215
useMediaQuery,
2316
} from '@mui/material';
2417
import HomeIcon from '@mui/icons-material/Home';
25-
import SystemUpdateIcon from '@mui/icons-material/SystemUpdateAlt';
2618
import TableViewIcon from '@mui/icons-material/TableView';
27-
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
2819
import { API_CONFIG, isElectron } from '../../../api-variable';
2920
import { TokenContext } from '../../context/TokenProvider';
3021
import { UnsavedContext } from '../../context/UnsavedContext';
@@ -40,37 +31,22 @@ import {
4031
useMounted,
4132
logError,
4233
Severity,
43-
infoMsg,
4434
exitApp,
4535
useMyNavigate,
4636
useWaitForRemoteQueue,
47-
Online,
4837
} from '../../utils';
4938
import { withBucket } from '../../hoc/withBucket';
50-
import {
51-
useLoadProjectData,
52-
useOfflineAvailToggle,
53-
useOfflnProjRead,
54-
usePlan,
55-
useVProjectRead,
56-
} from '../../crud';
39+
import { usePlan } from '../../crud';
5740
import Busy from '../Busy';
58-
import CloudOffIcon from '@mui/icons-material/CloudOff';
59-
import CloudOnIcon from '@mui/icons-material/Cloud';
6041
import ProjectDownloadAlert from '../ProjectDownloadAlert';
61-
import { axiosPost } from '../../utils/axios';
62-
import { DateTime } from 'luxon';
63-
import { useSnackBar, AlertSeverity } from '../../hoc/SnackBar';
42+
import { useSnackBar } from '../../hoc/SnackBar';
6443
import PolicyDialog from '../PolicyDialog';
6544
import JSONAPISource from '@orbit/jsonapi';
66-
import { mainSelector, sharedSelector, viewModeSelector } from '../../selector';
45+
import { viewModeSelector } from '../../selector';
6746
import { useHome } from '../../utils/useHome';
68-
import { useOrbitData } from '../../hoc/useOrbitData';
69-
import packageJson from '../../../package.json';
70-
import { MainAPI } from '@model/main-api';
7147
import { ApmLogo } from '../../control/ApmLogo';
7248
import { OrgHead } from './OrgHead';
73-
const ipc = window?.api as MainAPI;
49+
import { HeadStatus } from './HeadStatus';
7450

7551
const twoIcon = { minWidth: `calc(${48 * 2}px)` } as React.CSSProperties;
7652
const threeIcon = { minWidth: `calc(${48 * 3}px)` } as React.CSSProperties;
@@ -130,31 +106,25 @@ interface IProps {
130106
switchTo?: boolean;
131107
}
132108

133-
type DownloadAlertReason = 'cloud';
109+
export type DownloadAlertReason = 'cloud';
134110

135111
export const AppHead = (props: IProps) => {
136112
const { resetRequests, switchTo } = props;
137113
const orbitStatus = useSelector((state: IState) => state.orbit.status);
138114
const orbitErrorMsg = useSelector((state: IState) => state.orbit.message);
139-
const t: IMainStrings = useSelector(mainSelector, shallowEqual);
140-
const ts: ISharedStrings = useSelector(sharedSelector, shallowEqual);
141115
const { pathname } = useLocation();
142116
const navigate = useMyNavigate();
143117
const theme = useTheme();
144118
const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm'));
145-
const offlineProjects = useOrbitData<OfflineProject[]>('offlineproject');
146-
const [hasOfflineProjects, setHasOfflineProjects] = useState(false);
147119
const [home] = useGlobal('home'); //verified this is not used in a function 2/18/25
148120
const [orgRole] = useGlobal('orgRole'); //verified this is not used in a function 2/18/25
149-
const [connected, setConnected] = useGlobal('connected'); //verified this is not used in a function 2/18/25
150121
const [errorReporter] = useGlobal('errorReporter');
151122
const [coordinator] = useGlobal('coordinator');
152123
const [user] = useGlobal('user');
153124
const [, setProject] = useGlobal('project');
154-
const [plan, setPlan] = useGlobal('plan'); //verified this is not used in a function 2/18/25
125+
const [, setPlan] = useGlobal('plan'); //verified this is not used in a function 2/18/25
155126
const remote = coordinator?.getSource('remote') as JSONAPISource;
156127
const [isOffline] = useGlobal('offline'); //verified this is not used in a function 2/18/25
157-
const [isOfflineOnly] = useGlobal('offlineOnly'); //verified this is not used in a function 2/18/25
158128
const tokenCtx = useContext(TokenContext);
159129
const ctx = useContext(UnsavedContext);
160130
const { checkSavedFn, startSave, toolsChanged, anySaving } = ctx.state;
@@ -164,29 +134,19 @@ export const AppHead = (props: IProps) => {
164134
const [dataChangeCount] = useGlobal('dataChangeCount'); //verified this is not used in a function 2/18/25
165135
const [importexportBusy] = useGlobal('importexportBusy'); //verified this is not used in a function 2/18/25
166136
const [isChanged] = useGlobal('changed'); //verified this is only used in a useEffect
167-
const [lang] = useGlobal('lang');
168137
const getGlobal = useGetGlobal();
169138
const [doExit, setDoExit] = useState(false);
170139
const [exitAlert, setExitAlert] = useState(false);
171140
const isMounted = useMounted('apphead');
172141
const [version, setVersion] = useState('');
173-
const [updates] = useState(
174-
(localStorage.getItem('updates') || 'true') === 'true'
175-
);
176-
const [latestVersion, setLatestVersion] = useGlobal('latestVersion'); //verified this is not used in a function 2/18/25
177-
const [latestRelease, setLatestRelease] = useGlobal('releaseDate'); //verified this is not used in a function 2/18/25
142+
const [latestVersion, setLatestVersion] = useState('');
178143
const [complete] = useGlobal('progress'); //verified this is not used in a function 2/18/25
179144
const [downloadAlert, setDownloadAlert] = useState(false);
180145
const downloadAlertReason = useRef<DownloadAlertReason | null>(null);
181146
const [updateTipOpen, setUpdateTipOpen] = useState(false);
182147
const [showTerms, setShowTerms] = useState('');
183148
const waitForRemoteQueue = useWaitForRemoteQueue();
184149
const waitForDataChangesQueue = useWaitForRemoteQueue('datachanges');
185-
const offlineProjectRead = useOfflnProjRead();
186-
const LoadData = useLoadProjectData();
187-
const offlineAvailToggle = useOfflineAvailToggle();
188-
const { getPlan } = usePlan();
189-
const vProject = useVProjectRead();
190150
const [mobileView] = useGlobal('mobileView');
191151

192152
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -267,61 +227,6 @@ export const AppHead = (props: IProps) => {
267227
handleMenu(what);
268228
};
269229

270-
const cloudAction = () => {
271-
localStorage.setItem(
272-
'mode',
273-
getGlobal('offline') ||
274-
orbitStatus !== undefined ||
275-
!getGlobal('connected')
276-
? 'online-cloud'
277-
: 'online-local'
278-
);
279-
localStorage.setItem(LocalKey.plan, getGlobal('plan'));
280-
handleMenu('Logout', !isOffline ? 'cloud' : null);
281-
};
282-
283-
const handleSetOnline = (cb?: () => void) => {
284-
Online(true, (isConnected) => {
285-
if (getGlobal('connected') !== isConnected) {
286-
localStorage.setItem(LocalKey.connected, isConnected.toString());
287-
setConnected(isConnected);
288-
}
289-
if (!isConnected) {
290-
showMessage(ts.mustBeOnline);
291-
return;
292-
}
293-
cb && cb();
294-
});
295-
};
296-
297-
useEffect(() => {
298-
const value = offlineProjects.some((p) => p?.attributes?.offlineAvailable);
299-
if (value !== hasOfflineProjects) setHasOfflineProjects(value);
300-
// eslint-disable-next-line react-hooks/exhaustive-deps
301-
}, [offlineProjects]);
302-
303-
const handleCloud = () => {
304-
handleSetOnline(() => {
305-
const planRec = getGlobal('plan')
306-
? getPlan(getGlobal('plan'))
307-
: undefined;
308-
if (!planRec) {
309-
if (hasOfflineProjects) cloudAction();
310-
return;
311-
}
312-
const offlineProject = offlineProjectRead(vProject(planRec));
313-
if (offlineProject?.attributes?.offlineAvailable) {
314-
cloudAction();
315-
} else {
316-
LoadData(getGlobal('project'), () => {
317-
offlineAvailToggle(getGlobal('project')).then(() => {
318-
cloudAction();
319-
});
320-
});
321-
}
322-
});
323-
};
324-
325230
useEffect(() => {
326231
if (tokenCtx.state.expiresAt === -1) {
327232
handleMenu('Logout');
@@ -345,14 +250,6 @@ export const AppHead = (props: IProps) => {
345250
else setView('Logout');
346251
};
347252

348-
const handleDownloadClick = () => {
349-
if (ipc)
350-
ipc?.openExternal(
351-
'https://software.sil.org/audioprojectmanager/download/'
352-
);
353-
// remote?.getCurrentWindow().close();
354-
};
355-
356253
const handleUnload = (e: any) => {
357254
if (pathname === '/') return true;
358255
if (pathname.startsWith('/access')) return true;
@@ -385,9 +282,6 @@ export const AppHead = (props: IProps) => {
385282
setView('Access');
386283
}
387284
}
388-
setHasOfflineProjects(
389-
offlineProjects.some((p) => p?.attributes?.offlineAvailable)
390-
);
391285
return () => {
392286
window.removeEventListener('beforeunload', handleUnload);
393287
};
@@ -406,59 +300,6 @@ export const AppHead = (props: IProps) => {
406300
// eslint-disable-next-line react-hooks/exhaustive-deps
407301
}, [exitAlert, isChanged]);
408302

409-
useEffect(() => {
410-
isMounted() && setVersion(packageJson.version);
411-
// eslint-disable-next-line react-hooks/exhaustive-deps
412-
}, [isMounted]);
413-
414-
useEffect(() => {
415-
if (
416-
latestVersion === '' &&
417-
version !== '' &&
418-
updates &&
419-
localStorage.getItem(LocalKey.connected) !== 'false'
420-
) {
421-
const bodyFormData = new FormData();
422-
bodyFormData.append('env', navigator.userAgent);
423-
axiosPost('userversions/2/' + version, bodyFormData)
424-
.then((result) => {
425-
const response = result as {
426-
data: { desktopVersion: string; dateUpdated: string };
427-
};
428-
const lv = response?.data['desktopVersion'];
429-
let lr = response?.data['dateUpdated'];
430-
if (!lr.endsWith('Z')) lr += 'Z';
431-
lr = DateTime.fromISO(lr)
432-
.setLocale(lang)
433-
.toLocaleString(DateTime.DATE_SHORT);
434-
setLatestVersion(lv);
435-
setLatestRelease(lr);
436-
if (isElectron && lv?.split(' ')[0] !== version)
437-
showMessage(
438-
<span>
439-
{t.updateAvailable.replace('{0}', lv).replace('{1}', lr)}
440-
<IconButton
441-
id="systemUpdate"
442-
onClick={handleDownloadClick}
443-
component="span"
444-
>
445-
<SystemUpdateIcon color="primary" />
446-
</IconButton>
447-
</span>,
448-
AlertSeverity.Warning
449-
);
450-
})
451-
.catch((err) => {
452-
logError(
453-
Severity.error,
454-
errorReporter,
455-
infoMsg(err, 'userversions failed ' + navigator.userAgent)
456-
);
457-
});
458-
}
459-
// eslint-disable-next-line react-hooks/exhaustive-deps
460-
}, [updates, version, lang]);
461-
462303
useEffect(() => {
463304
setCssVars(
464305
latestVersion !== '' && latestVersion !== version && isElectron
@@ -481,15 +322,14 @@ export const AppHead = (props: IProps) => {
481322
// eslint-disable-next-line react-hooks/exhaustive-deps
482323
}, [orbitStatus, orbitErrorMsg]);
483324

484-
const handleUpdateOpen = () => setUpdateTipOpen(true);
485-
const handleUpdateClose = () => setUpdateTipOpen(pathname === '/');
486325
const handleTermsClose = () => setShowTerms('');
487326

488327
if (view === 'Error') navigate('/error');
489328
if (view === 'Logout') setTimeout(() => navigate('/logout'), 500);
490329
if (view === 'Access') setTimeout(() => navigate('/'), 200);
491330
if (view === 'Terms') navigate('/terms');
492331
if (view === 'Privacy') navigate('/privacy');
332+
493333
return !mobileView && !isMobileWidth ? (
494334
<AppBar
495335
position="fixed"
@@ -527,68 +367,12 @@ export const AppHead = (props: IProps) => {
527367
</>
528368
)}
529369
{'\u00A0'}
530-
{orbitStatus !== undefined || !connected ? (
531-
<IconButton onClick={() => handleSetOnline()}>
532-
<CloudOffIcon color="action" />
533-
</IconButton>
534-
) : (
535-
isElectron &&
536-
!isOfflineOnly &&
537-
localStorage.getItem(LocalKey.userId) &&
538-
(plan || hasOfflineProjects) && (
539-
<Button
540-
onClick={handleCloud}
541-
startIcon={
542-
isOffline ? (
543-
<CloudOffIcon color="action" />
544-
) : (
545-
<CloudOnIcon color="secondary" />
546-
)
547-
}
548-
>
549-
{isOffline ? t.goOnline : t.goOffline}
550-
</Button>
551-
)
552-
)}
553-
{latestVersion !== '' &&
554-
isElectron &&
555-
latestVersion?.split(' ')[0] !== version && (
556-
<Tooltip
557-
arrow
558-
placement="bottom-end"
559-
open={updateTipOpen}
560-
onOpen={handleUpdateOpen}
561-
onClose={handleUpdateClose}
562-
title={t.updateAvailable
563-
.replace('{0}', latestVersion)
564-
.replace('{1}', latestRelease)}
565-
>
566-
<IconButton id="systemUpdate" onClick={handleDownloadClick}>
567-
<SystemUpdateIcon color="primary" />
568-
</IconButton>
569-
</Tooltip>
570-
)}
571-
{latestVersion !== '' &&
572-
!isElectron &&
573-
latestVersion.split(' ')[0] !== version &&
574-
latestVersion?.split(' ').length > 1 && (
575-
<Tooltip
576-
arrow
577-
open={updateTipOpen}
578-
onOpen={handleUpdateOpen}
579-
onClose={handleUpdateClose}
580-
title={t.updateAvailable
581-
.replace('{0}', latestVersion)
582-
.replace('{1}', latestRelease)}
583-
>
584-
<IconButton
585-
id="systemUpdate"
586-
href="https://www.audioprojectmanager.org"
587-
>
588-
<ExitToAppIcon color="primary" />
589-
</IconButton>
590-
</Tooltip>
591-
)}
370+
<HeadStatus
371+
handleMenu={handleMenu}
372+
onVersion={setVersion}
373+
onLatestVersion={setLatestVersion}
374+
onUpdateTipOpen={setUpdateTipOpen}
375+
/>
592376
<HelpMenu
593377
online={!isOffline}
594378
sx={updateTipOpen && isElectron ? { top: '40px' } : {}}
@@ -619,6 +403,14 @@ export const AppHead = (props: IProps) => {
619403
</IconButton>
620404
<OrgHead />
621405
<GrowingSpacer />
406+
{!isMobileWidth && (
407+
<HeadStatus
408+
handleMenu={handleMenu}
409+
onVersion={setVersion}
410+
onLatestVersion={setLatestVersion}
411+
onUpdateTipOpen={setUpdateTipOpen}
412+
/>
413+
)}
622414
<HelpMenu
623415
online={!isOffline}
624416
sx={updateTipOpen && isElectron ? { top: '40px' } : {}}

0 commit comments

Comments
 (0)