Skip to content

Refactor/enum implicit values #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 6 additions & 15 deletions src/components/AudioLevelIndicator/AudioLevelIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { interval } from 'd3-timer';
import React, { useEffect, useRef, useState } from 'react';
import { AudioTrack, LocalAudioTrack, RemoteAudioTrack } from 'twilio-video';
import { interval } from 'd3-timer';
import useIsTrackEnabled from '../../hooks/useIsTrackEnabled/useIsTrackEnabled';
import useMediaStreamTrack from '../../hooks/useMediaStreamTrack/useMediaStreamTrack';

Expand All @@ -20,8 +20,7 @@ export function initializeAnalyser(stream: MediaStream) {

audioSource.connect(analyser);

// Here we provide a way for the audioContext to be closed.
// Closing the audioContext allows the unused audioSource to be garbage collected.
// Here we provide a way for the audioContext to be closed. Closing the audioContext allows the unused audioSource to be garbage collected.
stream.addEventListener('cleanup', () => {
if (audioContext.state !== 'closed') {
audioContext.close();
Expand All @@ -41,19 +40,14 @@ function AudioLevelIndicator({ audioTrack, color = 'white' }: { audioTrack?: Aud

useEffect(() => {
if (audioTrack && mediaStreamTrack && isTrackEnabled) {
// Here we create a new MediaStream from a clone of the mediaStreamTrack.
// A clone is created to allow multiple instances of this component for a single
// AudioTrack on iOS Safari. We only clone the mediaStreamTrack on iOS.
// Cloning the mediaStreamTrack allows for multiple instances of the AudioLevelIndicator component for a single AudioTrack on iOS Safari.
let newMediaStream = new MediaStream([isIOS ? mediaStreamTrack.clone() : mediaStreamTrack]);

// Here we listen for the 'stopped' event on the audioTrack. When the audioTrack is stopped,
// we stop the cloned track that is stored in 'newMediaStream'. It is important that we stop
// all tracks when they are not in use. Browsers like Firefox don't let you create a new stream
// from a new audio device while the active audio device still has active tracks.
// It is important that we stop all tracks when they are not in use. Browsers like Firefox
// don't let you create a new stream from a new audio device while the active audio device still has active tracks.
const stopAllMediaStreamTracks = () => {
if (isIOS) {
// If we are on iOS, then we want to stop the MediaStreamTrack that we have previously cloned.
// If we are not on iOS, then we do not stop the MediaStreamTrack since it is the original and still in use.
newMediaStream.getTracks().forEach(track => track.stop());
}
newMediaStream.dispatchEvent(new Event('cleanup')); // Stop the audioContext
Expand All @@ -62,16 +56,14 @@ function AudioLevelIndicator({ audioTrack, color = 'white' }: { audioTrack?: Aud

const reinitializeAnalyser = () => {
stopAllMediaStreamTracks();
// We only clone the mediaStreamTrack on iOS.

newMediaStream = new MediaStream([isIOS ? mediaStreamTrack.clone() : mediaStreamTrack]);
setAnalyser(initializeAnalyser(newMediaStream));
};

setAnalyser(initializeAnalyser(newMediaStream));

// Here we reinitialize the AnalyserNode on focus to avoid an issue in Safari
// where the analysers stop functioning when the user switches to a new tab
// and switches back to the app.
window.addEventListener('focus', reinitializeAnalyser);

return () => {
Expand Down Expand Up @@ -109,7 +101,6 @@ function AudioLevelIndicator({ audioTrack, color = 'white' }: { audioTrack?: Aud
}
}, [isTrackEnabled, analyser]);

// Each instance of this component will need a unique HTML ID
const clipPathId = `audio-level-clip-${getUniqueClipId()}`;

return isTrackEnabled ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
import { Message } from '@twilio/conversations';
import clsx from 'clsx';
import throttle from 'lodash.throttle';
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';

const styles = createStyles({
outerContainer: {
Expand Down Expand Up @@ -45,12 +45,6 @@ interface MessageListScrollContainerProps extends WithStyles<typeof styles> {
messages: Message[];
}

interface MessageListScrollContainerState {
isScrolledToBottom: boolean;
showButton: boolean;
messageNotificationCount: number;
}

/*
* This component is a scrollable container that wraps around the 'MessageList' component.
* The MessageList will ultimately grow taller than its container as it continues to receive
Expand All @@ -62,107 +56,81 @@ interface MessageListScrollContainerState {
*
* Note that this component is tested with Cypress only.
*/
export class MessageListScrollContainer extends React.Component<
MessageListScrollContainerProps,
MessageListScrollContainerState
> {
chatThreadRef = React.createRef<HTMLDivElement>();
state = { isScrolledToBottom: true, showButton: false, messageNotificationCount: 0 };

scrollToBottom() {
const innerScrollContainerEl = this.chatThreadRef.current;
const MessageListScrollContainer: React.FC<MessageListScrollContainerProps> = ({ classes, messages, children }) => {
const chatThreadRef = useRef<HTMLDivElement>(null);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
const [showButton, setShowButton] = useState(false);
const [messageNotificationCount, setMessageNotificationCount] = useState(0);

const scrollToBottom = () => {
const innerScrollContainerEl = chatThreadRef.current;
if (!innerScrollContainerEl) return;
innerScrollContainerEl.scrollTop = innerScrollContainerEl.scrollHeight;
}

componentDidMount() {
this.scrollToBottom();
this.chatThreadRef.current && this.chatThreadRef.current.addEventListener('scroll', this.handleScroll);
}

// This component updates as users send new messages:
componentDidUpdate(prevProps: MessageListScrollContainerProps, prevState: MessageListScrollContainerState) {
const hasNewMessages = this.props.messages.length !== prevProps.messages.length;

if (prevState.isScrolledToBottom && hasNewMessages) {
this.scrollToBottom();
} else if (hasNewMessages) {
const numberOfNewMessages = this.props.messages.length - prevProps.messages.length;

this.setState(previousState => ({
// If there's at least one new message, show the 'new message' button:
showButton: !previousState.isScrolledToBottom,
// If 'new message' button is visible,
// messageNotificationCount will be the number of previously unread messages + the number of new messages.
// Otherwise, messageNotificationCount is set to 1:
messageNotificationCount: previousState.showButton
? previousState.messageNotificationCount + numberOfNewMessages
: 1,
}));
}
}

handleScroll = throttle(() => {
const innerScrollContainerEl = this.chatThreadRef.current;
};

// Because this.handleScroll() is a throttled method,
// it's possible that it can be called after this component unmounts, and this element will be null.
// Therefore, if it doesn't exist, don't do anything:
const handleScroll = throttle(() => {
const innerScrollContainerEl = chatThreadRef.current;
if (!innerScrollContainerEl) return;

// On systems using display scaling, scrollTop may return a decimal value, so we need to account for this in the
// "isScrolledToBottom" calculation.
const isScrolledToBottom =
const scrolledToBottom =
Math.abs(
innerScrollContainerEl.clientHeight + innerScrollContainerEl.scrollTop - innerScrollContainerEl.scrollHeight
) < 1;

this.setState(prevState => ({
isScrolledToBottom,
showButton: isScrolledToBottom ? false : prevState.showButton,
}));
setIsScrolledToBottom(scrolledToBottom);
if (!scrolledToBottom) setShowButton(true);
}, 300);

handleClick = () => {
const innerScrollContainerEl = this.chatThreadRef.current;
if (!innerScrollContainerEl) return;

innerScrollContainerEl.scrollTo({ top: innerScrollContainerEl.scrollHeight, behavior: 'smooth' });
useEffect(() => {
scrollToBottom();
const innerScrollContainerEl = chatThreadRef.current;
if (innerScrollContainerEl) {
innerScrollContainerEl.addEventListener('scroll', handleScroll);
}
return () => {
if (innerScrollContainerEl) {
innerScrollContainerEl.removeEventListener('scroll', handleScroll);
}
};
}, []);

useEffect(() => {
const hasNewMessages = messages.length !== 0;
if (isScrolledToBottom && hasNewMessages) {
scrollToBottom();
} else if (hasNewMessages) {
const numberOfNewMessages = messages.length;
// If 'new message' btn is visible, messageNotificationCount will be the number of prev. unread msgs + the number of new msgs.
setShowButton(!isScrolledToBottom);
setMessageNotificationCount(showButton ? messageNotificationCount + numberOfNewMessages : 1);
}
}, [messages]);

this.setState({ showButton: false });
const handleClick = () => {
scrollToBottom();
setShowButton(false);
};

componentWillUnmount() {
const innerScrollContainerEl = this.chatThreadRef.current;
if (!innerScrollContainerEl) return;

innerScrollContainerEl.removeEventListener('scroll', this.handleScroll);
}

render() {
const { classes } = this.props;

return (
<div className={classes.outerContainer}>
<div className={classes.innerScrollContainer} ref={this.chatThreadRef} data-cy-message-list-inner-scroll>
<div className={classes.messageListContainer}>
{this.props.children}
<Button
className={clsx(classes.button, { [classes.showButton]: this.state.showButton })}
onClick={this.handleClick}
startIcon={<ArrowDownwardIcon />}
color="primary"
variant="contained"
data-cy-new-message-button
>
{this.state.messageNotificationCount} new message
{this.state.messageNotificationCount > 1 && 's'}
</Button>
</div>
return (
<div className={classes.outerContainer}>
<div className={classes.innerScrollContainer} ref={chatThreadRef} data-cy-message-list-inner-scroll>
<div className={classes.messageListContainer}>
{children}
<Button
className={clsx(classes.button, { [classes.showButton]: showButton })}
onClick={handleClick}
startIcon={<ArrowDownwardIcon />}
color="primary"
variant="contained"
data-cy-new-message-button
>
{messageNotificationCount} new message{messageNotificationCount > 1 && 's'}
</Button>
</div>
</div>
);
}
}
</div>
);
};

export default withStyles(styles)(MessageListScrollContainer);
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import React from 'react';
import DeviceSelectionScreen from './DeviceSelectionScreen';
import CircularProgress from '@material-ui/core/CircularProgress';
import { shallow } from 'enzyme';
import { Steps } from '../PreJoinScreens';
import { useAppState } from '../../../state';
import { setImmediate } from 'timers';
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
import ToggleVideoButton from '../../Buttons/ToggleVideoButton/ToggleVideoButton';
import { useAppState } from '../../../state';
import ToggleAudioButton from '../../Buttons/ToggleAudioButton/ToggleAudioButton';
import { setImmediate } from 'timers';
import ToggleVideoButton from '../../Buttons/ToggleVideoButton/ToggleVideoButton';
import { Steps } from '../PreJoinScreens';
import DeviceSelectionScreen, { JoiningMeetingAlert } from './DeviceSelectionScreen';

const mockUseAppState = useAppState as jest.Mock<any>;
const mockUseVideoContext = useVideoContext as jest.Mock<any>;
Expand Down Expand Up @@ -45,7 +43,7 @@ describe('the DeviceSelectionScreen component', () => {
const wrapper = shallow(<DeviceSelectionScreen name="test name" roomName="test room name" setStep={() => {}} />);

it('should show the loading screen', () => {
expect(wrapper.find(CircularProgress).exists()).toBe(true);
expect(wrapper.find(JoiningMeetingAlert).exists()).toBe(true);
});

it('should disable the desktop and mobile toggle video buttons', () => {
Expand Down Expand Up @@ -91,7 +89,7 @@ describe('the DeviceSelectionScreen component', () => {
const wrapper = shallow(<DeviceSelectionScreen name="test name" roomName="test room name" setStep={() => {}} />);

it('should show the loading screen', () => {
expect(wrapper.find(CircularProgress).exists()).toBe(true);
expect(wrapper.find(JoiningMeetingAlert).exists()).toBe(true);
});

it('should disable the desktop and mobile toggle video buttons', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import React from 'react';
import { makeStyles, Typography, Grid, Button, Theme, Hidden, Switch, Tooltip } from '@material-ui/core';
import { Button, Grid, Hidden, Switch, Theme, Tooltip, Typography, makeStyles } from '@material-ui/core';
import CircularProgress from '@material-ui/core/CircularProgress';
import Divider from '@material-ui/core/Divider';
import LocalVideoPreview from './LocalVideoPreview/LocalVideoPreview';
import SettingsMenu from './SettingsMenu/SettingsMenu';
import { Steps } from '../PreJoinScreens';
import ToggleAudioButton from '../../Buttons/ToggleAudioButton/ToggleAudioButton';
import ToggleVideoButton from '../../Buttons/ToggleVideoButton/ToggleVideoButton';
import { useAppState } from '../../../state';
import useChatContext from '../../../hooks/useChatContext/useChatContext';
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import useChatContext from '../../../hooks/useChatContext/useChatContext';
import { useKrispToggle } from '../../../hooks/useKrispToggle/useKrispToggle';
import SmallCheckIcon from '../../../icons/SmallCheckIcon';
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
import InfoIconOutlined from '../../../icons/InfoIconOutlined';
import SmallCheckIcon from '../../../icons/SmallCheckIcon';
import { useAppState } from '../../../state';
import ToggleAudioButton from '../../Buttons/ToggleAudioButton/ToggleAudioButton';
import ToggleVideoButton from '../../Buttons/ToggleVideoButton/ToggleVideoButton';
import { Steps } from '../PreJoinScreens';
import LocalVideoPreview from './LocalVideoPreview/LocalVideoPreview';
import SettingsMenu from './SettingsMenu/SettingsMenu';

const useStyles = makeStyles((theme: Theme) => ({
gutterBottom: {
Expand Down Expand Up @@ -91,18 +90,7 @@ export default function DeviceSelectionScreen({ name, roomName, setStep }: Devic
};

if (isFetching || isConnecting) {
return (
<Grid container justifyContent="center" alignItems="center" direction="column" style={{ height: '100%' }}>
<div>
<CircularProgress variant="indeterminate" />
</div>
<div>
<Typography variant="body2" style={{ fontWeight: 'bold', fontSize: '16px' }}>
Joining Meeting
</Typography>
</div>
</Grid>
);
return <JoiningMeetingAlert />;
}

return (
Expand Down Expand Up @@ -209,3 +197,18 @@ export default function DeviceSelectionScreen({ name, roomName, setStep }: Devic
</>
);
}

export const JoiningMeetingAlert = () => {
return (
<Grid container justifyContent="center" alignItems="center" direction="column" style={{ height: '100%' }}>
<div>
<CircularProgress variant="indeterminate" />
</div>
<div>
<Typography variant="body2" style={{ fontWeight: 'bold', fontSize: '16px' }}>
Joining Meeting
</Typography>
</div>
</Grid>
);
};
14 changes: 7 additions & 7 deletions src/components/PreJoinScreens/PreJoinScreens.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React, { useState, useEffect, FormEvent } from 'react';
import DeviceSelectionScreen from './DeviceSelectionScreen/DeviceSelectionScreen';
import { FormEvent, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import useVideoContext from '../../hooks/useVideoContext/useVideoContext';
import { useAppState } from '../../state';
import IntroContainer from '../IntroContainer/IntroContainer';
import DeviceSelectionScreen from './DeviceSelectionScreen/DeviceSelectionScreen';
import MediaErrorSnackbar from './MediaErrorSnackbar/MediaErrorSnackbar';
import RoomNameScreen from './RoomNameScreen/RoomNameScreen';
import { useAppState } from '../../state';
import { useParams } from 'react-router-dom';
import useVideoContext from '../../hooks/useVideoContext/useVideoContext';

export enum Steps {
roomNameStep,
deviceSelectionStep,
roomNameStep = 0,
deviceSelectionStep = 1,
}

export default function PreJoinScreens() {
Expand Down
Loading