Skip to content

Commit

Permalink
feat(react-native): add CallContent component with customization of i…
Browse files Browse the repository at this point in the history
…nner components (#933)

This PR primarily focuses on the introduction of the CallContent
component which is also the root component of the customization of inner
components.

This PR also removes the `fullscreen` mode from `LocalParticipantView`.
  • Loading branch information
khushal87 authored Aug 16, 2023
1 parent 32e8675 commit 1fe896f
Show file tree
Hide file tree
Showing 15 changed files with 395 additions and 318 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import mockParticipant from '../mocks/participant';
import { ComponentTestIds } from '../../src/constants/TestIds';
import { mockCall } from '../mocks/call';
import { act, render, screen, within } from '../utils/RNTLTools';
import { CallParticipantsGrid } from '../../src/components';
import {
CallParticipantsGrid,
CallParticipantsList,
LocalParticipantView,
ParticipantView,
VideoRenderer,
} from '../../src/components';
import { ViewToken } from 'react-native';

console.warn = jest.fn();
Expand All @@ -19,26 +25,6 @@ enum P_IDS {
}

describe('CallParticipantsGrid', () => {
it('should render an local video when only 1 participant present in the call', async () => {
const call = mockCall(mockClientWithUser(), [
mockParticipant({
isLocalParticipant: true,
sessionId: P_IDS.LOCAL_1,
userId: P_IDS.LOCAL_1,
publishedTracks: [SfuModels.TrackType.AUDIO],
videoStream: null,
}),
]);

render(<CallParticipantsGrid />, {
call,
});

expect(
await screen.findByTestId(ComponentTestIds.LOCAL_PARTICIPANT_FULLSCREEN),
).toBeVisible();
});

it('should render an call participants with grid mode with 2 participants when no screen shared', async () => {
const call = mockCall(mockClientWithUser(), [
mockParticipant({
Expand All @@ -53,9 +39,17 @@ describe('CallParticipantsGrid', () => {
}),
]);

render(<CallParticipantsGrid />, {
call,
});
render(
<CallParticipantsGrid
CallParticipantsList={CallParticipantsList}
ParticipantView={ParticipantView}
LocalParticipantView={LocalParticipantView}
VideoRenderer={VideoRenderer}
/>,
{
call,
},
);

expect(
await screen.findByTestId(ComponentTestIds.CALL_PARTICIPANTS_GRID),
Expand Down Expand Up @@ -108,9 +102,16 @@ describe('CallParticipantsGrid', () => {
}),
]);

render(<CallParticipantsGrid />, {
call,
});
render(
<CallParticipantsGrid
CallParticipantsList={CallParticipantsList}
ParticipantView={ParticipantView}
VideoRenderer={VideoRenderer}
/>,
{
call,
},
);

const visibleParticipantsItems = call.state.participants.map((p) => ({
key: p.sessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import mockParticipant from '../mocks/participant';
import { ComponentTestIds } from '../../src/constants/TestIds';
import { mockCall } from '../mocks/call';
import { render, screen } from '../utils/RNTLTools';
import { CallParticipantsSpotlight } from '../../src/components';
import {
CallParticipantsList,
CallParticipantsSpotlight,
ParticipantLabel,
ParticipantView,
VideoRenderer,
} from '../../src/components';

console.warn = jest.fn();
jest.useFakeTimers();
Expand Down Expand Up @@ -38,9 +44,17 @@ describe('CallParticipantsSpotlight', () => {
}),
]);

render(<CallParticipantsSpotlight />, {
call,
});
render(
<CallParticipantsSpotlight
CallParticipantsList={CallParticipantsList}
ParticipantView={ParticipantView}
VideoRenderer={VideoRenderer}
ParticipantLabel={ParticipantLabel}
/>,
{
call,
},
);

expect(
await screen.findByTestId(ComponentTestIds.PARTICIPANT_SCREEN_SHARING),
Expand Down Expand Up @@ -68,9 +82,15 @@ describe('CallParticipantsSpotlight', () => {
}),
]);

render(<CallParticipantsSpotlight />, {
call,
});
render(
<CallParticipantsSpotlight
CallParticipantsList={CallParticipantsList}
ParticipantView={ParticipantView}
/>,
{
call,
},
);

expect(
await screen.findByTestId(ComponentTestIds.CALL_PARTICIPANTS_SPOTLIGHT),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { useEffect, useRef } from 'react';
import { StyleSheet, View } from 'react-native';
import {
CallTopView as DefaultCallTopView,
ParticipantsInfoBadge as DefaultParticipantsInfoBadge,
CallTopViewProps,
} from '../CallTopView';
import {
CallParticipantsGrid,
CallParticipantsGridProps,
CallParticipantsSpotlight,
} from '../CallLayout';
import {
CallControls as DefaultCallControls,
CallControlProps,
} from '../CallControls';
import { CallParticipantsList as DefaultCallParticipantsList } from '../CallParticipantsList';
import {
useCall,
useHasOngoingScreenShare,
} from '@stream-io/video-react-bindings';
import {
ParticipantNetworkQualityIndicator as DefaultParticipantNetworkQualityIndicator,
ParticipantReaction as DefaultParticipantReaction,
ParticipantLabel as DefaultParticipantLabel,
ParticipantVideoFallback as DefaultParticipantVideoFallback,
VideoRenderer as DefaultVideoRenderer,
ParticipantView as DefaultParticipantView,
LocalParticipantView as DefaultLocalParticipantView,
} from '../../Participant';
import { CallingState } from '@stream-io/video-client';
import { useIncallManager } from '../../../hooks';

export type CallParticipantsComponentProps = Pick<
CallParticipantsGridProps,
| 'CallParticipantsList'
| 'LocalParticipantView'
| 'ParticipantLabel'
| 'ParticipantNetworkQualityIndicator'
| 'ParticipantReaction'
| 'ParticipantVideoFallback'
| 'ParticipantView'
| 'VideoRenderer'
> & {
/**
* Component to customize the CallTopView component.
*/
CallTopView?: React.ComponentType<CallTopViewProps>;
/**
* Component to customize the CallControls component.
*/
CallControls?: React.ComponentType<CallControlProps>;
};

export type CallContentProps = Pick<CallControlProps, 'onHangupCallHandler'> &
Pick<
CallTopViewProps,
'onBackPressed' | 'onParticipantInfoPress' | 'ParticipantsInfoBadge'
> &
CallParticipantsComponentProps & {
/**
* This switches the participant's layout between the grid and the spotlight mode.
*/
layout?: 'grid' | 'spotlight';
};

export const CallContent = ({
onBackPressed,
onParticipantInfoPress,
onHangupCallHandler,
CallParticipantsList = DefaultCallParticipantsList,
LocalParticipantView = DefaultLocalParticipantView,
ParticipantLabel = DefaultParticipantLabel,
ParticipantNetworkQualityIndicator = DefaultParticipantNetworkQualityIndicator,
ParticipantReaction = DefaultParticipantReaction,
ParticipantVideoFallback = DefaultParticipantVideoFallback,
ParticipantView = DefaultParticipantView,
ParticipantsInfoBadge = DefaultParticipantsInfoBadge,
VideoRenderer = DefaultVideoRenderer,
CallTopView = DefaultCallTopView,
CallControls = DefaultCallControls,
layout,
}: CallContentProps) => {
const hasScreenShare = useHasOngoingScreenShare();
const showSpotlightLayout = hasScreenShare || layout === 'spotlight';
/**
* This hook is used to handle IncallManager specs of the application.
*/
useIncallManager({ media: 'video', auto: true });

const call = useCall();
const activeCallRef = useRef(call);
activeCallRef.current = call;

useEffect(() => {
return () => {
if (activeCallRef.current?.state.callingState !== CallingState.LEFT) {
activeCallRef.current?.leave();
}
};
}, []);

const participantViewProps: CallParticipantsComponentProps = {
CallParticipantsList,
LocalParticipantView,
ParticipantLabel,
ParticipantNetworkQualityIndicator,
ParticipantReaction,
ParticipantVideoFallback,
ParticipantView,
VideoRenderer,
};

return (
<View style={styles.container}>
<View style={styles.container}>
<CallTopView
onBackPressed={onBackPressed}
onParticipantInfoPress={onParticipantInfoPress}
ParticipantsInfoBadge={ParticipantsInfoBadge}
/>
{showSpotlightLayout ? (
<CallParticipantsSpotlight {...participantViewProps} />
) : (
<CallParticipantsGrid {...participantViewProps} />
)}
</View>
<CallControls onHangupCallHandler={onHangupCallHandler} />
</View>
);
};

const styles = StyleSheet.create({
container: { flex: 1 },
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CallContent';
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,9 @@ import React from 'react';
import { StyleSheet, View } from 'react-native';
import { useCallStateHooks } from '@stream-io/video-react-bindings';
import { useDebouncedValue } from '../../../utils/hooks/useDebouncedValue';
import {
CallParticipantsList,
CallParticipantsListProps,
} from '../CallParticipantsList/CallParticipantsList';
import { CallParticipantsListProps } from '../CallParticipantsList/CallParticipantsList';
import { ComponentTestIds } from '../../../constants/TestIds';
import {
ParticipantNetworkQualityIndicator as DefaultParticipantNetworkQualityIndicator,
ParticipantReaction as DefaultParticipantReaction,
ParticipantLabel as DefaultParticipantLabel,
ParticipantVideoFallback as DefaultParticipantVideoFallback,
VideoRenderer as DefaultVideoRenderer,
ParticipantView as DefaultParticipantView,
LocalParticipantView as DefaultLocalParticipantView,
LocalParticipantViewProps,
} from '../../Participant';
import { LocalParticipantViewProps } from '../../Participant';

/**
* Props for the CallParticipantsGrid component.
Expand All @@ -31,51 +19,57 @@ export type CallParticipantsGridProps = Pick<
| 'VideoRenderer'
> & {
/**
* Component to customize the local participant view.
* Component to customize the LocalParticipantView.
*/
LocalParticipantView?: React.ComponentType<LocalParticipantViewProps>;
/**
* Component to customize the CallParticipantsList.
*/
CallParticipantsList?: React.ComponentType<CallParticipantsListProps>;
};

/**
* Component used to display the list of participants in a grid mode.
*/
export const CallParticipantsGrid = ({
ParticipantLabel = DefaultParticipantLabel,
ParticipantNetworkQualityIndicator = DefaultParticipantNetworkQualityIndicator,
ParticipantReaction = DefaultParticipantReaction,
ParticipantVideoFallback = DefaultParticipantVideoFallback,
ParticipantView = DefaultParticipantView,
VideoRenderer = DefaultVideoRenderer,
LocalParticipantView = DefaultLocalParticipantView,
CallParticipantsList,
ParticipantLabel,
ParticipantNetworkQualityIndicator,
ParticipantReaction,
ParticipantVideoFallback,
ParticipantView,
VideoRenderer,
LocalParticipantView,
}: CallParticipantsGridProps) => {
const { useRemoteParticipants, useParticipants } = useCallStateHooks();
const _remoteParticipants = useRemoteParticipants();
const allParticipants = useParticipants();
const remoteParticipants = useDebouncedValue(_remoteParticipants, 300); // we debounce the remote participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously

const showFloatingView = remoteParticipants.length < 3;
const isUserAloneInCall = remoteParticipants?.length === 0;
const participants = showFloatingView ? remoteParticipants : allParticipants;
const showFloatingView =
remoteParticipants.length > 0 && remoteParticipants.length < 3;

if (showFloatingView && isUserAloneInCall) {
return <LocalParticipantView layout={'fullscreen'} />;
}
const participants = showFloatingView ? remoteParticipants : allParticipants;

return (
<View
style={styles.container}
testID={ComponentTestIds.CALL_PARTICIPANTS_GRID}
>
{showFloatingView && <LocalParticipantView layout={'floating'} />}
<CallParticipantsList
participants={participants}
ParticipantLabel={ParticipantLabel}
ParticipantNetworkQualityIndicator={ParticipantNetworkQualityIndicator}
ParticipantReaction={ParticipantReaction}
ParticipantVideoFallback={ParticipantVideoFallback}
ParticipantView={ParticipantView}
VideoRenderer={VideoRenderer}
/>
{showFloatingView && LocalParticipantView && <LocalParticipantView />}
{CallParticipantsList && (
<CallParticipantsList
participants={participants}
ParticipantLabel={ParticipantLabel}
ParticipantNetworkQualityIndicator={
ParticipantNetworkQualityIndicator
}
ParticipantReaction={ParticipantReaction}
ParticipantVideoFallback={ParticipantVideoFallback}
ParticipantView={ParticipantView}
VideoRenderer={VideoRenderer}
/>
)}
</View>
);
};
Expand Down
Loading

0 comments on commit 1fe896f

Please sign in to comment.