diff --git a/src/components/Call/Call.js b/src/components/Call/Call.js
index 339ef66..a8c01b1 100644
--- a/src/components/Call/Call.js
+++ b/src/components/Call/Call.js
@@ -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 (
+
+ {
+ {
+ sendHello(screenShare.id);
+ }
+ }
+ />
+ }
+
+ );
+ } else {
+ const tiles = participants?.filter((p) => !p.local);
+ return (
+
+ {tiles?.map((t, i) => (
+ {
+ sendHello(t.id);
+ }
+ }
+ />
+ ))}
+
+ );
+ }
+ }, [participants, isScreenShare]);
+
+ const displaySmallTiles = useMemo(() => {
+ const isLarge = false;
+ if (isScreenShare) {
+ return (
+
+ {participants?.map((p, i) => (
+ {
+ sendHello(p.id);
+ }
+ }
+ />
+ ))}
+
+ );
+ } else {
+ const tiles = participants?.filter((p) => p.local);
+ return (
+
+ {tiles?.map((t, i) => (
+ {
+ sendHello(t.id);
+ }
+ }
+ />
+ ))}
+
+ );
+ }
+ }, [participants, isScreenShare]);
/**
* Send an app message to the remote participant whose tile was clicked on.
@@ -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 = (
- {
- 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 (
-
- {
- !message
- ? largeTiles
- : null /* Avoid showing large tiles to make room for the message */
- }
-
-
{smallTiles}
+ {displayLargeTiles}
+ {displaySmallTiles}
{message && (
-screen" for each shared screen
- */
-const initialCallState = {
- callItems: {
- local: {
- videoTrackState: null,
- audioTrackState: null,
- },
- },
- clickAllowTimeoutFired: false,
- camOrMicError: null,
- fatalError: null,
-};
-
-// --- Actions ---
-
-/**
- * CLICK_ALLOW_TIMEOUT action structure:
- * - type: string
- */
-const CLICK_ALLOW_TIMEOUT = 'CLICK_ALLOW_TIMEOUT';
-
-/**
- * PARTICIPANTS_CHANGE action structure:
- * - type: string
- * - participants: Object (from Daily callObject.participants())
- */
-const PARTICIPANTS_CHANGE = 'PARTICIPANTS_CHANGE';
-
-/**
- * CAM_OR_MIC_ERROR action structure:
- * - type: string
- * - message: string
- */
-const CAM_OR_MIC_ERROR = 'CAM_OR_MIC_ERROR';
-
-/**
- * CAM_OR_MIC_ERROR action structure:
- * - type: string
- * - message: string
- */
-const FATAL_ERROR = 'FATAL_ERROR';
-
-// --- Reducer and helpers --
-
-function callReducer(callState, action) {
- switch (action.type) {
- case CLICK_ALLOW_TIMEOUT:
- return {
- ...callState,
- clickAllowTimeoutFired: true,
- };
- case PARTICIPANTS_CHANGE:
- const callItems = getCallItems(action.participants);
- return {
- ...callState,
- callItems,
- };
- case CAM_OR_MIC_ERROR:
- return { ...callState, camOrMicError: action.message };
- case FATAL_ERROR:
- return { ...callState, fatalError: action.message };
- default:
- throw new Error();
- }
-}
-
-function getLocalCallItem(callItems) {
- return callItems['local'];
-}
-
-function getCallItems(participants) {
- let callItems = { ...initialCallState.callItems }; // Ensure we *always* have a local participant
- for (const [id, participant] of Object.entries(participants)) {
- callItems[id] = {
- videoTrackState: participant.tracks.video,
- audioTrackState: participant.tracks.audio,
- };
- if (shouldIncludeScreenCallItem(participant)) {
- callItems[id + '-screen'] = {
- videoTrackState: participant.tracks.screenVideo,
- audioTrackState: participant.tracks.screenAudio,
- };
- }
- }
- return callItems;
-}
-
-function shouldIncludeScreenCallItem(participant) {
- const trackStatesForInclusion = ['loading', 'playable', 'interrupted'];
- return (
- trackStatesForInclusion.includes(participant.tracks.screenVideo.state) ||
- trackStatesForInclusion.includes(participant.tracks.screenAudio.state)
- );
-}
-
-// --- Derived data ---
-
-// True if id corresponds to local participant (*not* their screen share)
-function isLocal(id) {
- return id === 'local';
-}
-
-function isScreenShare(id) {
- return id.endsWith('-screen');
-}
-
-function containsScreenShare(callItems) {
- return Object.keys(callItems).some((id) => isScreenShare(id));
-}
-
-function getMessage(callState) {
- function shouldShowClickAllow() {
- const localCallItem = getLocalCallItem(callState.callItems);
- const hasLoaded = localCallItem && !localCallItem.isLoading;
- return !hasLoaded && callState.clickAllowTimeoutFired;
- }
-
- let header = null;
- let detail = null;
- let isError = false;
- if (callState.fatalError) {
- header = `Fatal error: ${callState.fatalError}`;
- isError = true;
- } else if (callState.camOrMicError) {
- header = `Camera or mic access error: ${callState.camOrMicError}`;
- detail =
- 'See https://help.daily.co/en/articles/2528184-unblock-camera-mic-access-on-a-computer to troubleshoot.';
- isError = true;
- } else if (shouldShowClickAllow()) {
- header = 'Click "Allow" to enable camera and mic access';
- } else if (Object.keys(callState.callItems).length === 1) {
- header = "Copy and share this page's URL to invite others";
- detail = window.location.href;
- }
- return header || detail ? { header, detail, isError } : null;
-}
-
-export {
- initialCallState,
- CLICK_ALLOW_TIMEOUT,
- PARTICIPANTS_CHANGE,
- CAM_OR_MIC_ERROR,
- FATAL_ERROR,
- callReducer,
- isLocal,
- isScreenShare,
- containsScreenShare,
- getMessage,
-};
diff --git a/src/components/Tile/Tile.js b/src/components/Tile/Tile.js
index a6997da..031f32e 100644
--- a/src/components/Tile/Tile.js
+++ b/src/components/Tile/Tile.js
@@ -10,12 +10,14 @@ function getTrackUnavailableMessage(kind, trackState) {
} else if (trackState.blocked.byDeviceMissing) {
return `${kind} device missing`;
}
+ return `${kind} blocked`;
case 'off':
if (trackState.off.byUser) {
return `${kind} muted`;
} else if (trackState.off.byBandwidth) {
return `${kind} muted to save bandwidth`;
}
+ return `${kind} off`;
case 'sendable':
return `${kind} not subscribed`;
case 'loading':