Jeremy Kahn 291ed0c2b9
feat(connection-test): Display tracker connection status (#128)
* feat(ConnectionTest): track tracker state
* feat(ConnectionTest): show tracker searching state
* chore(deps): use github:jeremyckahn/trystero#feature/get-tracker-connections
* feat(connection-test): hide network indicator in non-room routes
* feat(connection-test): show peer searching status
* feat(connection-test): hide peer searching UI when not in a room
2023-07-13 09:50:54 -05:00

354 lines
10 KiB
TypeScript

import {
PropsWithChildren,
SyntheticEvent,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import CssBaseline from '@mui/material/CssBaseline'
import { ThemeProvider, createTheme } from '@mui/material/styles'
import Box from '@mui/material/Box'
import { AlertColor } from '@mui/material/Alert'
import { useWindowSize } from '@react-hook/window-size'
import { ShellContext } from 'contexts/ShellContext'
import { SettingsContext } from 'contexts/SettingsContext'
import { AlertOptions } from 'models/shell'
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
import { ErrorBoundary } from 'components/ErrorBoundary'
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
import { Drawer } from './Drawer'
import { UpgradeDialog } from './UpgradeDialog'
import { ShellAppBar } from './ShellAppBar'
import { NotificationArea } from './NotificationArea'
import { RouteContent } from './RouteContent'
import { PeerList } from './PeerList'
import { QRCodeDialog } from './QRCodeDialog'
import { RoomShareDialog } from './RoomShareDialog'
import { useConnectionTest } from './useConnectionTest'
export interface ShellProps extends PropsWithChildren {
userPeerId: string
appNeedsUpdate: boolean
}
export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const { getUserSettings, updateUserSettings } = useContext(SettingsContext)
const { colorMode } = getUserSettings()
const theme = useMemo(
() =>
createTheme({
palette: {
mode: colorMode,
},
}),
[colorMode]
)
const [windowWidth] = useWindowSize()
const defaultSidebarsOpen = windowWidth >= theme.breakpoints.values.lg
const [isAlertShowing, setIsAlertShowing] = useState(false)
const [isDrawerOpen, setIsDrawerOpen] = useState(defaultSidebarsOpen)
const [isQRCodeDialogOpen, setIsQRCodeDialogOpen] = useState(false)
const [isRoomShareDialogOpen, setIsRoomShareDialogOpen] = useState(false)
const [alertSeverity, setAlertSeverity] = useState<AlertColor>('info')
const [showAppBar, setShowAppBar] = useState(true)
const [showRoomControls, setShowRoomControls] = useState(true)
const [isFullscreen, setIsFullscreen] = useState(false)
const [title, setTitle] = useState('')
const [alertText, setAlertText] = useState('')
const [roomId, setRoomId] = useState<string | undefined>(undefined)
const [password, setPassword] = useState<string | undefined>(undefined)
const [isPeerListOpen, setIsPeerListOpen] = useState(defaultSidebarsOpen)
const [peerList, setPeerList] = useState<Peer[]>([]) // except self
const [peerConnectionTypes, setPeerConnectionTypes] = useState<
Record<string, PeerConnectionType>
>({})
const [tabHasFocus, setTabHasFocus] = useState(true)
const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED)
const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED)
const [screenState, setScreenState] = useState<ScreenShareState>(
ScreenShareState.NOT_SHARING
)
const [customUsername, setCustomUsername] = useState(
getUserSettings().customUsername
)
const [peerAudios, setPeerAudios] = useState<
Record<string, HTMLAudioElement>
>({})
const showAlert = useCallback<
(message: string, options?: AlertOptions) => void
>((message, options) => {
setAlertText(message)
setAlertSeverity(options?.severity ?? 'info')
setIsAlertShowing(true)
}, [])
const { connectionTestResults } = useConnectionTest()
const shellContextValue = useMemo(
() => ({
tabHasFocus,
showRoomControls,
setShowRoomControls,
setTitle,
showAlert,
isPeerListOpen,
setIsQRCodeDialogOpen,
roomId,
setRoomId,
password,
setPassword,
setIsPeerListOpen,
peerList,
setPeerList,
peerConnectionTypes,
setPeerConnectionTypes,
audioState,
setAudioState,
videoState,
setVideoState,
screenState,
setScreenState,
peerAudios,
setPeerAudios,
customUsername,
setCustomUsername,
connectionTestResults,
}),
[
isPeerListOpen,
setIsQRCodeDialogOpen,
roomId,
setRoomId,
password,
setPassword,
peerList,
peerConnectionTypes,
tabHasFocus,
showRoomControls,
setShowRoomControls,
setTitle,
showAlert,
audioState,
setAudioState,
videoState,
setVideoState,
screenState,
setScreenState,
peerAudios,
setPeerAudios,
customUsername,
setCustomUsername,
connectionTestResults,
]
)
const handleAlertClose = (
_event?: SyntheticEvent | Event,
reason?: string
) => {
if (reason === 'clickaway') {
return
}
setIsAlertShowing(false)
}
useEffect(() => {
if (customUsername === getUserSettings().customUsername) return
updateUserSettings({ customUsername })
}, [customUsername, getUserSettings, updateUserSettings])
useEffect(() => {
document.title = title
}, [title])
const enterFullscreen = async () => {
const body: any = document.body
try {
if (body.requestFullscreen) {
await body.requestFullscreen()
} else if (body.webkitRequestFullscreen) {
await body.webkitRequestFullscreen()
} else if (body.mozRequestFullScreen) {
await body.mozRequestFullScreen()
} else if (body.msRequestFullscreen) {
await body.msRequestFullscreen()
}
} catch (e) {
// Silence harmless errors
}
}
const exitFullscreen = async () => {
const document: any = window.document
try {
if (document.exitFullscreen) {
await document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
await document.webkitExitFullscreen()
} else if (document.mozCancelFullScreen) {
await document.mozCancelFullScreen()
} else if (document.msExitFullScreen) {
await document.msExitFullScreen()
}
} catch (e) {
// Silence harmless errors
}
}
useEffect(() => {
if (isFullscreen) {
enterFullscreen()
setShowRoomControls(false)
setShowAppBar(false)
} else {
exitFullscreen()
setShowAppBar(true)
setShowRoomControls(true)
}
}, [isFullscreen, setShowRoomControls, setShowAppBar])
useEffect(() => {
if (isFullscreen) setShowAppBar(showRoomControls)
}, [isFullscreen, showRoomControls, setShowAppBar])
useEffect(() => {
const handleFocus = () => {
setTabHasFocus(true)
}
const handleBlur = () => {
setTabHasFocus(false)
}
const handleFullscreen = () => {
setIsFullscreen(!!document.fullscreenElement)
}
window.addEventListener('focus', handleFocus)
window.addEventListener('blur', handleBlur)
document.addEventListener('fullscreenchange', handleFullscreen)
return () => {
window.removeEventListener('focus', handleFocus)
window.removeEventListener('blur', handleBlur)
document.removeEventListener('fullscreenchange', handleFullscreen)
}
}, [])
const handleDrawerOpen = () => {
setIsDrawerOpen(true)
}
const handleDrawerClose = () => {
setIsDrawerOpen(false)
}
const handlePeerListClick = () => {
setIsPeerListOpen(!isPeerListOpen)
}
const copyToClipboard = async (
content: string,
alert: string,
severity: AlertColor = 'success'
) => {
await navigator.clipboard.writeText(content)
shellContextValue.showAlert(alert, { severity })
}
const handleLinkButtonClick = async () => {
if (roomId !== undefined && password !== undefined) {
setIsRoomShareDialogOpen(true)
} else {
copyToClipboard(window.location.href, 'Current URL copied to clipboard')
}
}
const handleQRCodeDialogClose = () => {
setIsQRCodeDialogOpen(false)
}
const handleRoomShareDialogClose = () => {
setIsRoomShareDialogOpen(false)
}
return (
<ShellContext.Provider value={shellContextValue}>
<ThemeProvider theme={theme}>
<CssBaseline />
<UpgradeDialog appNeedsUpdate={appNeedsUpdate} />
<Box
className="Chitchatter"
sx={{
height: '100vh',
display: 'flex',
}}
>
<NotificationArea
alertSeverity={alertSeverity}
alertText={alertText}
isAlertShowing={isAlertShowing}
onAlertClose={handleAlertClose}
/>
<ShellAppBar
onDrawerOpen={handleDrawerOpen}
onLinkButtonClick={handleLinkButtonClick}
isDrawerOpen={isDrawerOpen}
isPeerListOpen={isPeerListOpen}
title={title}
onPeerListClick={handlePeerListClick}
onRoomControlsClick={() => setShowRoomControls(!showRoomControls)}
setIsQRCodeDialogOpen={setIsQRCodeDialogOpen}
showAppBar={showAppBar}
isFullscreen={isFullscreen}
setIsFullscreen={setIsFullscreen}
/>
<Drawer
isDrawerOpen={isDrawerOpen}
onDrawerClose={handleDrawerClose}
theme={theme}
/>
<RouteContent
isDrawerOpen={isDrawerOpen}
isPeerListOpen={isPeerListOpen}
showAppBar={showAppBar}
>
<ErrorBoundary>{children}</ErrorBoundary>
</RouteContent>
<PeerList
userId={userPeerId}
roomId={roomId}
isPeerListOpen={isPeerListOpen}
onPeerListClose={handlePeerListClick}
peerList={peerList}
peerConnectionTypes={peerConnectionTypes}
audioState={audioState}
peerAudios={peerAudios}
connectionTestResults={connectionTestResults}
/>
<QRCodeDialog
isOpen={isQRCodeDialogOpen}
handleClose={handleQRCodeDialogClose}
/>
<RoomShareDialog
isOpen={isRoomShareDialogOpen}
handleClose={handleRoomShareDialogClose}
roomId={roomId ?? ''}
password={password ?? ''}
showAlert={showAlert}
copyToClipboard={copyToClipboard}
/>
</Box>
</ThemeProvider>
</ShellContext.Provider>
)
}