Files
nirvana-v3/packages/desktop/src/tree/FooterControls.tsx
T
2022-07-05 11:13:50 -05:00

555 lines
16 KiB
TypeScript

import {
Avatar,
AvatarGroup,
Box,
Button,
CircularProgress,
Container,
Dialog,
Divider,
FormControl,
FormHelperText,
IconButton,
InputLabel,
MenuItem,
Select,
Stack,
Switch,
Tooltip,
Typography,
} from '@mui/material';
import {
FiChevronsLeft,
FiChevronsRight,
FiCircle,
FiRss,
FiSettings,
FiSun,
FiVideo,
FiVideoOff,
FiX,
FiZap,
FiZapOff,
} from 'react-icons/fi';
import { MasterConversation, RoomMap } from '../util/types';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ConversationUserMember } from '@nirvana/core/models/conversation.model';
import { SUPPORT_DISPLAY_NAME } from '../util/support';
import { blueGrey } from '@mui/material/colors';
import useAuth from '../providers/AuthProvider';
import useConversations from '../providers/ConversationProvider';
import useDevices from '../hooks/useDevices';
import useElectron from '../providers/ElectronProvider';
import useSearch from '../providers/SearchProvider';
import useZen from '../providers/ZenProvider';
export default function FooterControls() {
const { handleFlowState } = useZen();
const { selectedConversation, priorityConversations, userLocalStream, handleToggleVideo } =
useConversations();
const { user, handleLogout } = useAuth();
const { handleToggleDesktopMode, desktopMode } = useElectron();
const [openUserSettings, setOpenUserSettings] = useState<boolean>(false);
const handleTurnOnFlowState = useCallback(() => {
// when in overlay mode, we need to toggle main app and then go into flow state
if (desktopMode === 'overlayOnly') {
handleToggleDesktopMode();
}
handleFlowState();
}, [handleFlowState, desktopMode, handleToggleDesktopMode]);
const handleClickProfile = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
if (desktopMode === 'overlayOnly') {
handleToggleDesktopMode();
}
setOpenUserSettings(true);
},
[setOpenUserSettings, handleToggleDesktopMode, desktopMode],
);
const handleCloseUserSettings = useCallback(() => {
setOpenUserSettings(false);
}, [setOpenUserSettings]);
// is there a selected conversation and if so, is it a priority one?
const isSelectedConversationPriority = useMemo(() => {
const priorityIds =
priorityConversations.map((priorityConvo) => priorityConvo._id.toString()) ?? [];
if (selectedConversation && priorityIds.includes(selectedConversation._id.toString())) {
return true;
}
return false;
}, [selectedConversation, priorityConversations]);
const localVideoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (localVideoRef?.current && userLocalStream) {
localVideoRef.current.srcObject = userLocalStream;
}
}, [localVideoRef, userLocalStream]);
return (
<Box
sx={{
maxWidth: 80,
width: '100%',
zIndex: 10,
boxShadow: 10,
bgcolor: blueGrey[800],
display: 'flex',
flexDirection: 'column',
}}
className={'titlebar'}
>
<Stack
direction={'column'}
alignItems={'center'}
justifyContent={'flex-start'}
sx={{
p: 1,
flex: 1,
overflow: 'auto',
}}
>
{priorityConversations.map((masterPriorityConversation) => (
<OverlayConversation
key={`priorityConversationOverlay-${masterPriorityConversation._id.toString()}`}
masterConversation={masterPriorityConversation}
isSelected={
masterPriorityConversation._id.toString() === selectedConversation?._id.toString()
}
/>
))}
{selectedConversation && !isSelectedConversationPriority && (
<>
<Divider orientation="horizontal" flexItem />
<OverlayConversation masterConversation={selectedConversation} isSelected={true} />
</>
)}
</Stack>
<Stack sx={{ mt: 'auto' }} spacing={1} direction={'column'} alignItems={'center'}>
<Divider orientation="horizontal" flexItem />
<Tooltip title="Show video!">
<IconButton size={'small'} sx={{ color: 'white' }} onClick={handleToggleVideo}>
{userLocalStream ? <FiVideo /> : <FiVideoOff />}
</IconButton>
</Tooltip>
<Divider orientation="horizontal" flexItem />
<Button size={'small'} color={'secondary'} variant="text" onClick={handleTurnOnFlowState}>
flow
</Button>
<IconButton
onClick={handleClickProfile}
size="small"
sx={{ borderRadius: 1 }}
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
{userLocalStream ? (
<Box height={40} width={40} ref={localVideoRef} component={'video'} autoPlay muted />
) : (
<Avatar alt={user.givenName} src={user.picture} />
)}
</IconButton>
<Divider orientation="horizontal" flexItem />
<Tooltip title={desktopMode === 'overlayOnly' ? 'expand' : 'collapse'}>
<IconButton color={'info'} onClick={handleToggleDesktopMode}>
{desktopMode === 'overlayOnly' ? <FiChevronsLeft /> : <FiChevronsRight />}
</IconButton>
</Tooltip>
</Stack>
<UserSettingsDialog open={openUserSettings} handleClose={handleCloseUserSettings} />
</Box>
);
}
function OverlayConversation({
masterConversation,
isSelected = false,
}: {
masterConversation: MasterConversation;
isSelected?: boolean;
}) {
const { user } = useAuth();
const { selectConversation, handleEscape, updateConversationPriority, userLocalStream } =
useConversations();
const handleSelectConversation = useCallback(() => {
if (!isSelected) {
selectConversation(masterConversation._id.toString(), false);
}
}, [masterConversation, selectConversation, isSelected]);
const isPriority = useMemo(() => {
const personalConvoMember = masterConversation.members.find(
(mem) => mem._id.toString() === user._id.toString(),
);
if (personalConvoMember && personalConvoMember.memberState === 'priority') {
return true;
}
return false;
}, [masterConversation, user]);
const handleToggleConversationZap = useCallback(() => {
if (isPriority) {
updateConversationPriority(masterConversation._id.toString(), 'inbox');
return;
}
updateConversationPriority(masterConversation._id.toString(), 'priority');
}, [updateConversationPriority, isPriority, masterConversation._id]);
const userVideoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
//
}, []);
return (
<Stack
direction={'column'}
spacing={1}
sx={{
mb: 1,
p: 1,
borderRadius: 2,
bgcolor: isSelected ? blueGrey[300] : blueGrey[600],
transition: (theme) =>
theme.transitions.create(['background-color', 'transform'], {
duration: theme.transitions.duration.standard,
}),
cursor: 'pointer',
'&:hover': {
bgcolor: blueGrey[300],
},
}}
onClick={handleSelectConversation}
alignItems={'center'}
>
{/* status */}
<Box
component={'span'}
sx={{
bgcolor: (theme) => theme.palette.secondary.main,
height: 5,
width: 5,
borderRadius: 100,
}}
/>
<OverlayConversationAvatars
conversationUserMembers={masterConversation.members}
isConversationSelected={isSelected}
tunedInUserIds={masterConversation.tunedInUsers}
roomPeers={masterConversation.room}
localStreamToPeers={masterConversation.localStream}
/>
{isSelected && (
<>
<Divider orientation="horizontal" flexItem />
<Tooltip title={'enter to toggle, ~ to push to talk'}>
<IconButton color={'primary'} size="small">
{isPriority ? <FiSun /> : <FiCircle />}
</IconButton>
</Tooltip>
<Divider orientation="horizontal" flexItem />
<Tooltip title={isPriority ? 'zapped' : 'un-zapped'}>
<IconButton
sx={{
color: 'white',
fontSize: 15,
}}
aria-selected={isPriority ? true : false}
onClick={handleToggleConversationZap}
>
{isPriority ? <FiZap /> : <FiZapOff />}
</IconButton>
</Tooltip>
<Tooltip title="hit escape!">
<IconButton
sx={{
color: 'GrayText',
}}
size="small"
onClick={handleEscape}
>
<FiX />
</IconButton>
</Tooltip>
</>
)}
</Stack>
);
}
function OverlayConversationAvatars({
conversationUserMembers,
tunedInUserIds,
isConversationSelected,
roomPeers,
localStreamToPeers,
}: {
isConversationSelected: boolean;
tunedInUserIds: string[];
conversationUserMembers: ConversationUserMember[];
roomPeers: RoomMap;
localStreamToPeers: MediaStream;
}) {
const { user } = useAuth();
const sortedMembers = useMemo(() => {
const sortedUserMembers = [...conversationUserMembers];
sortedUserMembers.sort((convoUserA, convoUserB) => {
if (convoUserA._id.toString() === user?._id.toString()) return 2;
if (tunedInUserIds?.includes(convoUserA._id.toString())) return 1;
if (tunedInUserIds?.includes(convoUserB._id.toString())) return -1;
return -2;
});
return sortedUserMembers;
}, [conversationUserMembers, user, tunedInUserIds]);
console.log(roomPeers);
return (
<AvatarGroup
variant={'rounded'}
sx={{
display: 'flex',
flexDirection: 'column',
'& >:first-child': {
marginTop: 0,
},
'& > *': {
marginLeft: '0 !important' as any,
marginTop: -2,
},
}}
max={3}
>
{sortedMembers?.map((conversationUserMember, index) => (
<OverlayConversationAvatar
key={`${conversationUserMember._id.toString()}-overlayConvoAvatar`}
isTunedIn={tunedInUserIds?.includes(conversationUserMember._id.toString())}
isConversationSelected={isConversationSelected}
conversationUserMember={conversationUserMember}
isConnecting={
(roomPeers && roomPeers[conversationUserMember._id.toString()]?.isConnecting) ?? false
}
localStreamToPeers={localStreamToPeers}
/>
))}
</AvatarGroup>
);
}
function OverlayConversationAvatar({
conversationUserMember,
isTunedIn,
isConversationSelected,
isConnecting,
localStreamToPeers,
}: {
conversationUserMember: ConversationUserMember;
isTunedIn: boolean;
isConversationSelected: boolean; // if user is connecting or just have this conversation selected
isConnecting: boolean; // if we are calling or receiving call from this person and still connecting
localStreamToPeers: MediaStream; // the local stream that is casted to each peer
}) {
const { user } = useAuth();
const localVideoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (localVideoRef?.current && localStreamToPeers) {
localVideoRef.current.srcObject = localStreamToPeers;
}
}, [localVideoRef, localStreamToPeers]);
console.log(isConnecting);
if (isConnecting) {
return <CircularProgress />;
}
if (conversationUserMember._id.toString() === user._id.toString() && localStreamToPeers) {
return (
<Box
height={40}
width={40}
ref={localVideoRef}
component={'video'}
autoPlay
muted
border={'2px solid #fff'}
/>
);
}
return (
<Avatar
alt={conversationUserMember?.givenName}
src={conversationUserMember?.picture}
sx={{
opacity: isTunedIn ? '100%' : '20%',
}}
/>
);
return <></>;
}
const NO_DEVICE_SELECTION = 'NA';
function UserSettingsDialog({ open, handleClose }: { handleClose: () => void; open: boolean }) {
const { user, handleLogout } = useAuth();
const devices = useDevices();
const { omniSearch } = useSearch();
const handleQuickDialSupport = useCallback(() => {
omniSearch(SUPPORT_DISPLAY_NAME);
handleClose();
}, [omniSearch, handleClose]);
return (
<Dialog maxWidth={'sm'} open={open} fullWidth onClose={handleClose}>
<Container
maxWidth={false}
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: 2,
flex: 1,
background: blueGrey[50],
position: 'relative',
}}
>
<IconButton sx={{ position: 'absolute', top: 10, right: 10 }} onClick={handleClose}>
<FiX />
</IconButton>
<Container
maxWidth="sm"
sx={{
display: 'flex',
flexDirection: 'column',
p: 10,
gap: 2,
}}
>
<Stack flexDirection={'row'} justifyContent={'space-between'}>
<Typography variant="h6">Media Settings</Typography>
<Button variant={'text'} color={'secondary'}>
Unplug everything
</Button>
</Stack>
<Stack direction="row" alignItems="flex-start" spacing={1}>
<FormControl fullWidth>
<InputLabel>Microphone</InputLabel>
<Select label="Microphone">
<MenuItem value={NO_DEVICE_SELECTION}>
<em>None</em>
</MenuItem>
{devices.audioInputDevices.map((deviceInfo) => (
<MenuItem
key={`micDeviceSelection-${deviceInfo.deviceId}`}
value={deviceInfo.deviceId}
>
{deviceInfo.label}
</MenuItem>
))}
</Select>
<FormHelperText>
Please select distinct speaker and microphone to avoid distortion when you have a
bluetooth device connected.
</FormHelperText>
</FormControl>
<FormControl fullWidth>
<InputLabel>Video</InputLabel>
<Select label="Audio">
<MenuItem value={NO_DEVICE_SELECTION}>
<em>None</em>
</MenuItem>
{devices.videoDevices.map((deviceInfo) => (
<MenuItem
key={`videoDeviceSelection-${deviceInfo.deviceId}`}
value={deviceInfo.deviceId}
>
{deviceInfo.label}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
<Divider />
<Stack flexDirection={'row'} justifyContent={'space-between'} alignItems={'center'}>
<Typography variant="subtitle2">Questions?</Typography>
<Button variant={'text'}>Resources</Button>
</Stack>
<Divider />
<Stack flexDirection={'row'} justifyContent={'space-between'} alignItems={'center'}>
<Typography variant="subtitle2">Help, feedback, and more?</Typography>
<Button variant={'text'} onClick={handleQuickDialSupport}>
Contact Us
</Button>
</Stack>
<Divider />
<Stack flexDirection={'row'} justifyContent={'space-between'} alignItems={'center'}>
<Typography variant="subtitle2">Account</Typography>
<Button variant={'text'} onClick={handleLogout} color={'error'}>
log out
</Button>
</Stack>
</Container>
</Container>
</Dialog>
);
}