diff --git a/.gitignore b/.gitignore index 85162c36d..56c9c369b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ yarn-error.log* .env .env.* .vscode +*.csv test-reports junit.xml @@ -35,3 +36,6 @@ serviceAccountKey.json public/noisecancellation/ public/virtualbackground/ +files_smells.csv +components_smells.csv +*.components_smells.csv diff --git a/package-lock.json b/package-lock.json index b932e37f8..8fbc07b20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "twilio-video-app-react", - "version": "0.10.1", + "version": "0.10.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "twilio-video-app-react", - "version": "0.10.1", + "version": "0.10.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/src/components/AudioLevelIndicator/AudioLevelIndicator.tsx b/src/components/AudioLevelIndicator/AudioLevelIndicator.tsx index 2927b0a0b..2bc02ce46 100644 --- a/src/components/AudioLevelIndicator/AudioLevelIndicator.tsx +++ b/src/components/AudioLevelIndicator/AudioLevelIndicator.tsx @@ -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'; @@ -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(); @@ -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 @@ -62,7 +56,7 @@ 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)); }; @@ -70,8 +64,6 @@ function AudioLevelIndicator({ audioTrack, color = 'white' }: { audioTrack?: Aud 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 () => { @@ -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 ? ( diff --git a/src/components/ChatWindow/MessageList/MessageList.tsx b/src/components/ChatWindow/MessageList/MessageList.tsx index e4866f0cf..bb25d3d38 100644 --- a/src/components/ChatWindow/MessageList/MessageList.tsx +++ b/src/components/ChatWindow/MessageList/MessageList.tsx @@ -1,10 +1,10 @@ -import React from 'react'; import { Message } from '@twilio/conversations'; +import React from 'react'; +import useVideoContext from '../../../hooks/useVideoContext/useVideoContext'; +import MediaMessage from './MediaMessage/MediaMessage'; import MessageInfo from './MessageInfo/MessageInfo'; import MessageListScrollContainer from './MessageListScrollContainer/MessageListScrollContainer'; import TextMessage from './TextMessage/TextMessage'; -import useVideoContext from '../../../hooks/useVideoContext/useVideoContext'; -import MediaMessage from './MediaMessage/MediaMessage'; interface MessageListProps { messages: Message[]; @@ -15,7 +15,8 @@ const getFormattedTime = (message?: Message) => export default function MessageList({ messages }: MessageListProps) { const { room } = useVideoContext(); - const localParticipant = room!.localParticipant; + + if (!room) return <>; return ( @@ -26,15 +27,17 @@ export default function MessageList({ messages }: MessageListProps) { // Display the MessageInfo component when the author or formatted timestamp differs from the previous message const shouldDisplayMessageInfo = time !== previousTime || message.author !== messages[idx - 1]?.author; - const isLocalParticipant = localParticipant.identity === message.author; + const isLocalParticipant = room.localParticipant.identity === message.author; return ( - {shouldDisplayMessageInfo && ( - + {shouldDisplayMessageInfo && message.author && ( + + )} + {message.type === 'text' && message.body && ( + )} - {message.type === 'text' && } - {message.type === 'media' && } + {message.type === 'media' && message.attachedMedia && } ); })} diff --git a/src/components/ChatWindow/MessageList/MessageListScrollContainer/MessageListScrollContainer.tsx b/src/components/ChatWindow/MessageList/MessageListScrollContainer/MessageListScrollContainer.tsx index 0aad04dd4..66dd85c39 100644 --- a/src/components/ChatWindow/MessageList/MessageListScrollContainer/MessageListScrollContainer.tsx +++ b/src/components/ChatWindow/MessageList/MessageListScrollContainer/MessageListScrollContainer.tsx @@ -1,11 +1,11 @@ /* istanbul ignore file */ -import React from 'react'; -import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'; import Button from '@material-ui/core/Button'; -import clsx from 'clsx'; +import { WithStyles, createStyles, withStyles } from '@material-ui/core/styles'; +import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'; import { Message } from '@twilio/conversations'; +import clsx from 'clsx'; import throttle from 'lodash.throttle'; -import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles'; +import React, { useEffect, useRef, useState } from 'react'; const styles = createStyles({ outerContainer: { @@ -45,12 +45,6 @@ interface MessageListScrollContainerProps extends WithStyles { 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 @@ -62,103 +56,81 @@ interface MessageListScrollContainerState { * * Note that this component is tested with Cypress only. */ -export class MessageListScrollContainer extends React.Component< - MessageListScrollContainerProps, - MessageListScrollContainerState -> { - chatThreadRef = React.createRef(); - state = { isScrolledToBottom: true, showButton: false, messageNotificationCount: 0 }; - scrollToBottom() { - const innerScrollContainerEl = this.chatThreadRef.current!; - innerScrollContainerEl.scrollTop = innerScrollContainerEl!.scrollHeight; - } +const MessageListScrollContainer: React.FC = ({ classes, messages, children }) => { + const chatThreadRef = useRef(null); + const [isScrolledToBottom, setIsScrolledToBottom] = useState(true); + const [showButton, setShowButton] = useState(false); + const [messageNotificationCount, setMessageNotificationCount] = useState(0); - componentDidMount() { - this.scrollToBottom(); - 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, - })); - } - } + const scrollToBottom = () => { + const innerScrollContainerEl = chatThreadRef.current; + if (!innerScrollContainerEl) return; + innerScrollContainerEl.scrollTop = innerScrollContainerEl.scrollHeight; + }; - 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 + 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!; - - 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!; - - innerScrollContainerEl.removeEventListener('scroll', this.handleScroll); - } - - render() { - const { classes } = this.props; - - return ( -
-
-
- {this.props.children} - -
+ return ( +
+
+
+ {children} +
- ); - } -} +
+ ); +}; export default withStyles(styles)(MessageListScrollContainer); diff --git a/src/components/GalleryView/GalleryView.tsx b/src/components/GalleryView/GalleryView.tsx index 6f7f4f78a..194bec110 100644 --- a/src/components/GalleryView/GalleryView.tsx +++ b/src/components/GalleryView/GalleryView.tsx @@ -1,17 +1,16 @@ -import React from 'react'; +import { IconButton, Theme, createStyles, makeStyles } from '@material-ui/core'; import ArrowBack from '@material-ui/icons/ArrowBack'; import ArrowForward from '@material-ui/icons/ArrowForward'; +import { Pagination } from '@material-ui/lab'; import clsx from 'clsx'; import { GALLERY_VIEW_ASPECT_RATIO, GALLERY_VIEW_MARGIN } from '../../constants'; -import { IconButton, makeStyles, createStyles, Theme } from '@material-ui/core'; -import { Pagination } from '@material-ui/lab'; -import Participant from '../Participant/Participant'; +import useDominantSpeaker from '../../hooks/useDominantSpeaker/useDominantSpeaker'; import useGalleryViewLayout from '../../hooks/useGalleryViewLayout/useGalleryViewLayout'; -import useVideoContext from '../../hooks/useVideoContext/useVideoContext'; import useParticipantsContext from '../../hooks/useParticipantsContext/useParticipantsContext'; -import { usePagination } from './usePagination/usePagination'; -import useDominantSpeaker from '../../hooks/useDominantSpeaker/useDominantSpeaker'; +import useVideoContext from '../../hooks/useVideoContext/useVideoContext'; import { useAppState } from '../../state'; +import Participant from '../Participant/Participant'; +import { usePagination } from './usePagination/usePagination'; const CONTAINER_GUTTER = '50px'; @@ -85,10 +84,10 @@ export function GalleryView() { const { galleryViewParticipants } = useParticipantsContext(); const dominantSpeaker = useDominantSpeaker(true); - const { paginatedParticipants, setCurrentPage, currentPage, totalPages } = usePagination([ - room!.localParticipant, - ...galleryViewParticipants, - ]); + const participants = + room && room.localParticipant ? [room.localParticipant, ...galleryViewParticipants] : [...galleryViewParticipants]; + + const { paginatedParticipants, setCurrentPage, currentPage, totalPages } = usePagination(participants); const galleryViewLayoutParticipantCount = currentPage === 1 ? paginatedParticipants.length : maxGalleryViewParticipants; @@ -97,6 +96,10 @@ export function GalleryView() { const participantWidth = `${participantVideoWidth}px`; const participantHeight = `${Math.floor(participantVideoWidth * GALLERY_VIEW_ASPECT_RATIO)}px`; + if (!room) { + return <>; + } + return (
@@ -136,7 +139,7 @@ export function GalleryView() { >
diff --git a/src/components/GalleryView/usePagination/usePagination.ts b/src/components/GalleryView/usePagination/usePagination.ts index 70d476848..f4164ba79 100644 --- a/src/components/GalleryView/usePagination/usePagination.ts +++ b/src/components/GalleryView/usePagination/usePagination.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Participant } from 'twilio-video'; import { useAppState } from '../../../state'; diff --git a/src/components/IntroContainer/UserMenu/UserMenu.tsx b/src/components/IntroContainer/UserMenu/UserMenu.tsx index 60596f8f7..64f17e872 100644 --- a/src/components/IntroContainer/UserMenu/UserMenu.tsx +++ b/src/components/IntroContainer/UserMenu/UserMenu.tsx @@ -1,10 +1,10 @@ -import React, { useState, useRef, useCallback } from 'react'; -import { makeStyles, Typography, Button, MenuItem, Link } from '@material-ui/core'; +import { Button, Link, MenuItem, Typography, makeStyles } from '@material-ui/core'; +import Menu from '@material-ui/core/Menu'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import React, { useCallback, useRef, useState } from 'react'; +import useVideoContext from '../../../hooks/useVideoContext/useVideoContext'; import { useAppState } from '../../../state'; import UserAvatar from './UserAvatar/UserAvatar'; -import Menu from '@material-ui/core/Menu'; -import useVideoContext from '../../../hooks/useVideoContext/useVideoContext'; const useStyles = makeStyles({ userContainer: { @@ -38,6 +38,10 @@ const UserMenu: React.FC = () => { signOut?.(); }, [localTracks, signOut]); + if (!user) { + return <>; + } + if (process.env.REACT_APP_SET_AUTH === 'passcode') { return (
@@ -53,7 +57,7 @@ const UserMenu: React.FC = () => {
{ setMenuOpen(false); - if (isRecording) { - updateRecordingRules(room!.sid, [{ type: 'exclude', all: true }]); - } else { - updateRecordingRules(room!.sid, [{ type: 'include', all: true }]); + if (room) { + if (isRecording) { + updateRecordingRules(room.sid, [{ type: 'exclude', all: true }]); + } else { + updateRecordingRules(room.sid, [{ type: 'include', all: true }]); + } } }} data-cy-recording-button diff --git a/src/components/MenuBar/MenuBar.tsx b/src/components/MenuBar/MenuBar.tsx index fa03fd176..07affb62b 100644 --- a/src/components/MenuBar/MenuBar.tsx +++ b/src/components/MenuBar/MenuBar.tsx @@ -1,18 +1,17 @@ -import React from 'react'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import { Grid, Hidden, Typography } from '@material-ui/core'; import Button from '@material-ui/core/Button'; -import EndCallButton from '../Buttons/EndCallButton/EndCallButton'; -import { isMobile } from '../../utils'; -import Menu from './Menu/Menu'; import useParticipants from '../../hooks/useParticipants/useParticipants'; import useRoomState from '../../hooks/useRoomState/useRoomState'; import useVideoContext from '../../hooks/useVideoContext/useVideoContext'; -import { Typography, Grid, Hidden } from '@material-ui/core'; +import { isMobile } from '../../utils'; +import EndCallButton from '../Buttons/EndCallButton/EndCallButton'; import ToggleAudioButton from '../Buttons/ToggleAudioButton/ToggleAudioButton'; import ToggleChatButton from '../Buttons/ToggleChatButton/ToggleChatButton'; import ToggleVideoButton from '../Buttons/ToggleVideoButton/ToggleVideoButton'; import ToggleScreenShareButton from '../Buttons/ToogleScreenShareButton/ToggleScreenShareButton'; +import Menu from './Menu/Menu'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -71,6 +70,10 @@ export default function MenuBar() { const { room } = useVideoContext(); const participants = useParticipants(); + if (!room) { + return <>; + } + return ( <> {isSharingScreen && ( @@ -84,7 +87,7 @@ export default function MenuBar() { - {room!.name} | {participants.length + 1} participant{participants.length ? 's' : ''} + {room.name} | {participants.length + 1} participant{participants.length ? 's' : ''} diff --git a/src/components/MobileTopMenuBar/MobileTopMenuBar.tsx b/src/components/MobileTopMenuBar/MobileTopMenuBar.tsx index 6ce27c0b3..ae697eeee 100644 --- a/src/components/MobileTopMenuBar/MobileTopMenuBar.tsx +++ b/src/components/MobileTopMenuBar/MobileTopMenuBar.tsx @@ -1,5 +1,4 @@ import { Grid, makeStyles, Theme, Typography } from '@material-ui/core'; -import React from 'react'; import useVideoContext from '../../hooks/useVideoContext/useVideoContext'; import EndCallButton from '../Buttons/EndCallButton/EndCallButton'; import Menu from '../MenuBar/Menu/Menu'; @@ -34,9 +33,11 @@ export default function MobileTopMenuBar() { const classes = useStyles(); const { room } = useVideoContext(); + if (!room) return <>; + return ( - {room!.name} + {room.name}
diff --git a/src/components/ParticipantList/ParticipantList.tsx b/src/components/ParticipantList/ParticipantList.tsx index d4779117b..10b716e77 100644 --- a/src/components/ParticipantList/ParticipantList.tsx +++ b/src/components/ParticipantList/ParticipantList.tsx @@ -1,12 +1,11 @@ -import React from 'react'; +import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'; import clsx from 'clsx'; -import Participant from '../Participant/Participant'; -import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; import useMainParticipant from '../../hooks/useMainParticipant/useMainParticipant'; import useParticipantsContext from '../../hooks/useParticipantsContext/useParticipantsContext'; +import useScreenShareParticipant from '../../hooks/useScreenShareParticipant/useScreenShareParticipant'; import useVideoContext from '../../hooks/useVideoContext/useVideoContext'; +import Participant from '../Participant/Participant'; import useSelectedParticipant from '../VideoProvider/useSelectedParticipant/useSelectedParticipant'; -import useScreenShareParticipant from '../../hooks/useScreenShareParticipant/useScreenShareParticipant'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -44,7 +43,7 @@ const useStyles = makeStyles((theme: Theme) => export default function ParticipantList() { const classes = useStyles(); const { room } = useVideoContext(); - const localParticipant = room!.localParticipant; + const localParticipant = room && room.localParticipant; const { speakerViewParticipants } = useParticipantsContext(); const [selectedParticipant, setSelectedParticipant] = useSelectedParticipant(); const screenShareParticipant = useScreenShareParticipant(); @@ -53,6 +52,10 @@ export default function ParticipantList() { if (speakerViewParticipants.length === 0) return null; // Don't render this component if there are no remote participants. + if (!localParticipant) { + return <>; + } + return (