Conditionally show sidebar view in group calls

This commit is contained in:
trevor-signal
2026-01-20 12:29:25 -05:00
committed by GitHub
parent 680304f9d2
commit 20644761b0
4 changed files with 104 additions and 14 deletions

View File

@@ -327,6 +327,20 @@ export function CallScreen({
setShowRaisedHandsList(prevValue => !prevValue);
}, []);
const [sidebarViewDiffersFromGridView, setSidebarViewDiffersFromGridView] =
useState(false);
// If the user is in Sidebar view but it's no longer different from Grid view,
// automatically switch to Grid view.
useEffect(() => {
if (
!sidebarViewDiffersFromGridView &&
activeCall.viewMode === CallViewMode.Sidebar
) {
changeCallView(CallViewMode.Paginated);
}
}, [sidebarViewDiffersFromGridView, activeCall.viewMode, changeCallView]);
const [controlsHover, setControlsHover] = useState(false);
const onControlsMouseEnter = useCallback(() => {
setControlsHover(true);
@@ -445,7 +459,7 @@ export function CallScreen({
}, [showReactionPicker]);
useScreenSharingStoppedToast({ activeCall, i18n });
useViewModeChangedToast({ activeCall, i18n });
useViewModeChangedToast({ activeCall, i18n, sidebarViewDiffersFromGridView });
const currentPresenter = remoteParticipants.find(
participant => participant.presenting
@@ -993,6 +1007,9 @@ export function CallScreen({
imageDataCache={imageDataCache}
i18n={i18n}
joinedAt={activeCall.joinedAt}
onSidebarViewDiffersFromGridViewChange={
setSidebarViewDiffersFromGridView
}
remoteParticipants={activeCall.remoteParticipants}
setGroupCallVideoRequest={setGroupCallVideoRequest}
remoteAudioLevels={activeCall.remoteAudioLevels}
@@ -1073,6 +1090,7 @@ export function CallScreen({
i18n={i18n}
isGroupCall={isGroupCall}
participantCount={participantCount}
showSidebarViewOption={sidebarViewDiffersFromGridView}
togglePip={togglePip}
toggleSettings={toggleSettings}
/>
@@ -1320,9 +1338,11 @@ function renderDuration(ms: number): string {
function useViewModeChangedToast({
activeCall,
i18n,
sidebarViewDiffersFromGridView,
}: {
activeCall: ActiveCallType;
i18n: LocalizerType;
sidebarViewDiffersFromGridView: boolean;
}): void {
const { viewMode } = activeCall;
const previousViewMode = usePrevious(viewMode, viewMode);
@@ -1342,6 +1362,14 @@ function useViewModeChangedToast({
return;
}
if (
!sidebarViewDiffersFromGridView &&
previousViewMode === CallViewMode.Sidebar &&
viewMode === CallViewMode.Paginated
) {
return;
}
hideToast(VIEW_MODE_CHANGED_TOAST_KEY);
showToast({
key: VIEW_MODE_CHANGED_TOAST_KEY,
@@ -1364,6 +1392,7 @@ function useViewModeChangedToast({
hideToast,
i18n,
activeCall,
sidebarViewDiffersFromGridView,
viewMode,
previousViewMode,
presenterAci,

View File

@@ -16,11 +16,13 @@ export default {
argTypes: {
isGroupCall: { control: { type: 'boolean' } },
participantCount: { control: { type: 'number' } },
showSidebarViewOption: { control: { type: 'boolean' } },
},
args: {
i18n,
isGroupCall: false,
participantCount: 0,
showSidebarViewOption: false,
togglePip: action('toggle-pip'),
callViewMode: CallViewMode.Paginated,
changeCallView: action('change-call-view'),

View File

@@ -15,6 +15,7 @@ export type PropsType = {
isGroupCall?: boolean;
onCancel?: () => void;
participantCount: number;
showSidebarViewOption?: boolean;
togglePip?: () => void;
toggleSettings: () => void;
changeCallView?: (mode: CallViewMode) => void;
@@ -27,6 +28,7 @@ export function CallingHeader({
isGroupCall = false,
onCancel,
participantCount,
showSidebarViewOption = false,
togglePip,
toggleSettings,
}: PropsType): React.JSX.Element {
@@ -47,12 +49,16 @@ export function CallingHeader({
onClick: () => changeCallView(CallViewMode.Paginated),
value: CallViewMode.Paginated,
},
{
icon: 'CallSettingsButton__Icon--SidebarView',
label: i18n('icu:calling__view_mode--overflow'),
onClick: () => changeCallView(CallViewMode.Sidebar),
value: CallViewMode.Sidebar,
},
...(showSidebarViewOption
? [
{
icon: 'CallSettingsButton__Icon--SidebarView',
label: i18n('icu:calling__view_mode--overflow'),
onClick: () => changeCallView(CallViewMode.Sidebar),
value: CallViewMode.Sidebar,
},
]
: []),
{
icon: 'CallSettingsButton__Icon--SpeakerView',
label: i18n('icu:calling__view_mode--speaker'),

View File

@@ -68,6 +68,7 @@ type PropsType = {
imageDataCache: React.RefObject<CallingImageDataCache>;
isCallReconnecting: boolean;
joinedAt: number | null;
onSidebarViewDiffersFromGridViewChange: (differs: boolean) => void;
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
setGroupCallVideoRequest: (
_: Array<GroupCallVideoRequest>,
@@ -121,6 +122,7 @@ export function GroupCallRemoteParticipants({
i18n,
isCallReconnecting,
joinedAt,
onSidebarViewDiffersFromGridViewChange,
remoteParticipants,
setGroupCallVideoRequest,
remoteAudioLevels,
@@ -140,10 +142,7 @@ export function GroupCallRemoteParticipants({
const { invisibleDemuxIds, onParticipantVisibilityChanged } =
useInvisibleParticipants(remoteParticipants);
const minRenderedHeight =
callViewMode === CallViewMode.Paginated
? SMALL_TILES_MIN_HEIGHT
: LARGE_TILES_MIN_HEIGHT;
const minRenderedHeight = getMinRenderedHeight(callViewMode);
const isInSpeakerView =
callViewMode === CallViewMode.Speaker ||
@@ -157,9 +156,7 @@ export function GroupCallRemoteParticipants({
// 1. Figure out the maximum number of possible rows that could fit on the page.
// Could be 0 if (a) there are no participants (b) the container's height is small.
const maxRowsPerPage = Math.floor(
maxGridHeight / (minRenderedHeight + PARTICIPANT_MARGIN)
);
const maxRowsPerPage = getMaxRowsPerPage(maxGridHeight, minRenderedHeight);
// 2. Sort the participants in priority order: by `presenting` first, since presenters
// should be on the main grid, then by `speakerTime` so that the most recent speakers
@@ -228,6 +225,49 @@ export function GroupCallRemoteParticipants({
setPageIndex(gridParticipantsByPage.length - 1);
}
// Calculate whether Sidebar view would look different from Grid (Paginated) view.
// They differ when Grid view would have multiple pages.
const minRenderedHeightForGridView = getMinRenderedHeight(
CallViewMode.Paginated
);
const maxRowsPerPageForGridView = getMaxRowsPerPage(
maxGridHeight,
minRenderedHeightForGridView
);
const sidebarViewDiffersFromGridView = useMemo(() => {
if (!prioritySortedParticipants.length || !maxRowsPerPageForGridView) {
return false;
}
// When in Grid view, we've already calculated with the right params
if (isInPaginationView) {
return gridParticipantsByPage.length > 1;
}
// For other views, calculate how many would fit on the first page of Grid view
const gridViewPages = getGridParticipantsByPage({
participants: prioritySortedParticipants,
maxRowWidth,
maxPages: 1,
maxRowsPerPage: maxRowsPerPageForGridView,
minRenderedHeight: minRenderedHeightForGridView,
maxParticipantsPerPage: MAX_PARTICIPANTS_PER_PAGE,
});
return gridViewPages[0].numParticipants < prioritySortedParticipants.length;
}, [
isInPaginationView,
gridParticipantsByPage.length,
maxRowWidth,
maxRowsPerPageForGridView,
minRenderedHeightForGridView,
prioritySortedParticipants,
]);
useEffect(() => {
onSidebarViewDiffersFromGridViewChange(sidebarViewDiffersFromGridView);
}, [onSidebarViewDiffersFromGridViewChange, sidebarViewDiffersFromGridView]);
const totalParticipantsInGrid = gridParticipantsByPage.reduce(
(pageCount, { numParticipants }) => pageCount + numParticipants,
0
@@ -635,6 +675,19 @@ function participantWidthAtHeight(
return participant.videoAspectRatio * height;
}
function getMinRenderedHeight(callViewMode: CallViewMode): number {
return callViewMode === CallViewMode.Paginated
? SMALL_TILES_MIN_HEIGHT
: LARGE_TILES_MIN_HEIGHT;
}
function getMaxRowsPerPage(
maxGridHeight: number,
minRenderedHeight: number
): number {
return Math.floor(maxGridHeight / (minRenderedHeight + PARTICIPANT_MARGIN));
}
function stableParticipantComparator(
a: Readonly<{ demuxId: number }>,
b: Readonly<{ demuxId: number }>