Files
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

114 lines
4.6 KiB
TypeScript

import { useCallback, useMemo } from 'react';
import { useSelector, useStore } from 'react-redux';
import { IReduxState } from '../app/types';
import {
getDominantSpeakerParticipant,
getLocalParticipant,
getLocalScreenShareParticipant
} from '../base/participants/functions';
import { removeSecondScreen, setSecondScreen } from './actions.web';
import {
getSecondScreenShowing,
isSecondScreenEnabled,
pickSecondScreenTarget
} from './functions.web';
import { ISecondScreenSource } from './types';
/**
* Returns the ordered participant ids for a second-screen layout: the local
* participant first, then your own shared screen (a virtual participant) when
* sharing, then the remote participants. Mirrors the main filmstrip ordering.
*
* The remote ids come from the filmstrip's {@code remoteParticipants} array,
* which is reassigned immutably on join/leave (the base participants map is
* mutated in place, so selecting it directly would not re-render on membership
* changes).
*
* @returns {string[]}
*/
export function useSecondScreenParticipantIds(): string[] {
const localId = useSelector((state: IReduxState) => getLocalParticipant(state)?.id);
const localScreenShareId = useSelector((state: IReduxState) => getLocalScreenShareParticipant(state)?.id);
const remoteParticipantIds = useSelector((state: IReduxState) => state['features/filmstrip'].remoteParticipants);
return useMemo(() => {
const ids: string[] = [];
if (localId) {
ids.push(localId);
}
if (localScreenShareId) {
ids.push(localScreenShareId);
}
return ids.concat(remoteParticipantIds);
}, [ localId, localScreenShareId, remoteParticipantIds ]);
}
/**
* Returns the id of the conference dominant speaker, or {@code null}. Drives the
* speaking ring on the second-screen tiles.
*
* @returns {string | null}
*/
export function useDominantSpeakerId(): string | null {
return useSelector((state: IReduxState) => getDominantSpeakerParticipant(state)?.id ?? null);
}
/**
* Backs the in-app "send to second screen" triggers: whether to show one,
* whether the source is already on a screen, and the handler that toggles it.
* The handler dispatches {@code setSecondScreen}, the same action the external
* API dispatches, so the API stays the single control plane;
* {@link pickSecondScreenTarget} chooses which window it lands on.
*
* The trigger is shown whenever the feature is enabled and the browser can
* place a window on another screen, without checking that a second monitor is
* actually attached: that answer needs the window-management permission, and
* the click itself is what should ask for it. With no second monitor the window
* opens on the current screen.
*
* @param {ISecondScreenSource} source - What to send.
* @returns {Object} Whether the trigger is visible, whether its source is
* already on a second screen, and its click handler.
*/
export function useSendToSecondScreen(source: ISecondScreenSource): {
active: boolean; onClick: () => void; visible: boolean;
} {
const store = useStore<IReduxState>();
const visible = useSelector(isSecondScreenEnabled);
const activeId = useSelector((state: IReduxState) => getSecondScreenShowing(state, source));
const onClick = useCallback(() => {
// Already on a screen: the trigger turns it off again. This is the only
// way to close an in-app second screen from the meeting window, short of
// closing the popup by hand on the other display.
if (activeId) {
store.dispatch(removeSecondScreen(activeId));
return;
}
// Dispatch synchronously, always. window.open has to run in this click's
// own task for the popup blocker to see it as user-initiated, and on a
// profile that has not granted window-management yet, obtaining the
// screen details here would spend that activation waiting on a
// permission prompt: Chromium expires it after 5s and the prompt sits
// until the user answers it. The open asks for the permission itself and
// places the window once it has an answer (see openSecondScreenWindow),
// which is also why the target picked here has no screen index on that
// first send: without the details there are no screens to choose between,
// and the open puts it on the first external one.
const { id, screenId } = pickSecondScreenTarget(store.getState());
store.dispatch(setSecondScreen(id, source, screenId));
}, [ activeId, source, store ]);
return { active: Boolean(activeId),
onClick,
visible };
}