Skip to content
Open
316 changes: 182 additions & 134 deletions src/components/Call/Call.js
Original file line number Diff line number Diff line change
@@ -1,123 +1,211 @@
import React, { useEffect, useContext, useReducer, useCallback } from 'react';
import React, {
useEffect,
useContext,
useState,
useCallback,
useMemo,
} from 'react';
import './Call.css';
import Tile from '../Tile/Tile';
import CallObjectContext from '../../CallObjectContext';
import CallMessage from '../CallMessage/CallMessage';
import {
initialCallState,
CLICK_ALLOW_TIMEOUT,
PARTICIPANTS_CHANGE,
CAM_OR_MIC_ERROR,
FATAL_ERROR,
callReducer,
isLocal,
isScreenShare,
containsScreenShare,
getMessage,
} from './callState';
import { logDailyEvent } from '../../logUtils';

export default function Call() {
const callObject = useContext(CallObjectContext);
const [callState, dispatch] = useReducer(callReducer, initialCallState);

/**
* Start listening for participant changes, when the callObject is set.
*/
useEffect(() => {
if (!callObject) return;
const [participantUpdated, setParticipantUpdated] = useState(null);
const [participants, setParticipants] = useState([]);

const handleTrackStarted = useCallback((e) => {
logDailyEvent(e);
setParticipantUpdated(
`track-started-${e?.participant?.user_id}-${Date.now()}`
);
}, []);

const events = [
'participant-joined',
'participant-updated',
'participant-left',
];
const handleTrackStopped = useCallback((e) => {
logDailyEvent(e);
setParticipantUpdated(
`track-stopped-${e?.participant?.user_id}-${Date.now()}`
);
}, []);

function handleNewParticipantsState(event) {
event && logDailyEvent(event);
dispatch({
type: PARTICIPANTS_CHANGE,
participants: callObject.participants(),
});
}
const handleParticipantUpdate = useCallback((e) => {
logDailyEvent(e);
setParticipantUpdated(
`participant-updated-${e?.participant?.user_id}-${Date.now()}`
);
}, []);

// Use initial state
handleNewParticipantsState();
const handleErrorEvent = useCallback((e) => {
logDailyEvent(e);
getMessage(e);
}, []);

// Listen for changes in state
for (const event of events) {
callObject.on(event, handleNewParticipantsState);
}
const getMessage = (e) => {
let header = null;
let detail = null;
let isError = false;

// Stop listening for changes in state
return function cleanup() {
for (const event of events) {
callObject.off(event, handleNewParticipantsState);
if (!e) {
if (participants.length <= 1) {
header = "Copy and share this page's URL to invite others";
detail = window.location.href;
}
};
}, [callObject]);

/**
* Start listening for call errors, when the callObject is set.
*/
useEffect(() => {
if (!callObject) return;

function handleCameraErrorEvent(event) {
logDailyEvent(event);
dispatch({
type: CAM_OR_MIC_ERROR,
message:
(event && event.errorMsg && event.errorMsg.errorMsg) || 'Unknown',
});
} else if (e.action === 'error') {
header = `Fatal error ${(e && e.errorMsg) || 'Unknown'}`;
} else if (e.action === 'camera-error') {
header = `Camera or mic access error: ${
(e && e.errorMsg && e.errorMsg.errorMsg) || 'Unknown'
}`;
detail =
'See https://help.daily.co/en/articles/2528184-unblock-camera-mic-access-on-a-computer to troubleshoot.';
isError = true;
}

// We're making an assumption here: there is no camera error when callObject
// is first assigned.

callObject.on('camera-error', handleCameraErrorEvent);

return function cleanup() {
callObject.off('camera-error', handleCameraErrorEvent);
};
}, [callObject]);
return header || detail ? { header, detail, isError } : null;
};

/**
* Start listening for fatal errors, when the callObject is set.
* When the call object is set, listen and respond to events
*/
useEffect(() => {
if (!callObject) return;

function handleErrorEvent(e) {
logDailyEvent(e);
dispatch({
type: FATAL_ERROR,
message: (e && e.errorMsg) || 'Unknown',
});
}

// We're making an assumption here: there is no error when callObject is
// first assigned.

callObject.on('track-started', handleTrackStarted);
callObject.on('track-stopped', handleTrackStopped);
callObject.on('participant-updated', handleParticipantUpdate);
callObject.on('error', handleErrorEvent);
callObject.on('camera-error', handleErrorEvent);

return function cleanup() {
return () => {
callObject.off('track-started', handleTrackStarted);
callObject.off('track-stopped', handleTrackStopped);
callObject.off('participant-updated', handleParticipantUpdate);
callObject.off('error', handleErrorEvent);
callObject.off('camera-error', handleErrorEvent);
};
}, [callObject]);
}, [
callObject,
participants,
handleTrackStarted,
handleParticipantUpdate,
handleTrackStopped,
handleErrorEvent,
]);

/**
* Start a timer to show the "click allow" message, when the component mounts.
* Update participants for any event that happens to keep local participant list up to date.
* We grab the whole participant list to make sure everyone's status is most up to date.
*/
useEffect(() => {
const t = setTimeout(() => {
dispatch({ type: CLICK_ALLOW_TIMEOUT });
}, 2500);
if (participantUpdated) {
const list = Object.values(callObject?.participants());
setParticipants(list);
}
}, [participantUpdated, callObject]);

return function cleanup() {
clearTimeout(t);
};
}, []);
const isScreenShare = useMemo(() => {
return participants?.some((p) => p?.tracks?.screenVideo?.track);
}, [participants, callObject]);

const displayLargeTiles = useMemo(() => {
const isLarge = true;
if (isScreenShare) {
const screenShare = participants?.find(
(p) => p?.tracks?.screenVideo?.track
);
return (
<div className="large-tiles">
{
<Tile
key={`screenshare`}
videoTrackState={screenShare?.tracks?.screenVideo}
audioTrackState={screenShare?.tracks?.audio}
isLarge={isLarge}
disableCornerMessage={isScreenShare}
onClick={
screenShare.local
? null
: () => {
sendHello(screenShare.id);
}
}
/>
}
</div>
);
} else {
const tiles = participants?.filter((p) => !p.local);
return (
<div className="large-tiles">
{tiles?.map((t, i) => (
<Tile
key={`large-${i}`}
videoTrackState={t?.tracks?.video}
audioTrackState={t?.tracks?.audio}
isLarge={isLarge}
disableCornerMessage={isScreenShare}
onClick={
t.local
? null
: () => {
sendHello(t.id);
}
}
/>
))}
</div>
);
}
}, [participants, isScreenShare]);

const displaySmallTiles = useMemo(() => {
const isLarge = false;
if (isScreenShare) {
return (
<div className="small-tiles">
{participants?.map((p, i) => (
<Tile
key={`small-${i}`}
videoTrackState={p.tracks.video}
audioTrackState={p.tracks.audio}
isLocalPerson={p.local}
isLarge={isLarge}
disableCornerMessage={false}
onClick={
p.local
? null
: () => {
sendHello(p.id);
}
}
/>
))}
</div>
);
} else {
const tiles = participants?.filter((p) => p.local);
return (
<div className="small-tiles">
{tiles?.map((t, i) => (
<Tile
key={`small-${i}`}
videoTrackState={t.tracks.video}
audioTrackState={t.tracks.audio}
isLarge={isLarge}
disableCornerMessage={false}
onClick={
t.local
? null
: () => {
sendHello(t.id);
}
}
/>
))}
</div>
);
}
}, [participants, isScreenShare]);

/**
* Send an app message to the remote participant whose tile was clicked on.
Expand All @@ -130,51 +218,11 @@ export default function Call() {
[callObject]
);

function getTiles() {
let largeTiles = [];
let smallTiles = [];
Object.entries(callState.callItems).forEach(([id, callItem]) => {
const isLarge =
isScreenShare(id) ||
(!isLocal(id) && !containsScreenShare(callState.callItems));
const tile = (
<Tile
key={id}
videoTrackState={callItem.videoTrackState}
audioTrackState={callItem.audioTrackState}
isLocalPerson={isLocal(id)}
isLarge={isLarge}
disableCornerMessage={isScreenShare(id)}
onClick={
isLocal(id)
? null
: () => {
sendHello(id);
}
}
/>
);
if (isLarge) {
largeTiles.push(tile);
} else {
smallTiles.push(tile);
}
});
return [largeTiles, smallTiles];
}

const [largeTiles, smallTiles] = getTiles();
const message = getMessage(callState);
const message = getMessage();
return (
<div className="call">
<div className="large-tiles">
{
!message
? largeTiles
: null /* Avoid showing large tiles to make room for the message */
}
</div>
<div className="small-tiles">{smallTiles}</div>
{displayLargeTiles}
{displaySmallTiles}
{message && (
<CallMessage
header={message.header}
Expand Down
Loading