mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-20 12:50:19 +00:00
Compare commits
3 Commits
9030
...
fix/consol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
303f81f427 | ||
|
|
23a65fbf2a | ||
|
|
5fee521938 |
@@ -2253,32 +2253,6 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the external application that a file has been uploaded.
|
||||
*
|
||||
* @param {Object} fileMetadata - The file metadata.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyFileUploaded(fileMetadata) {
|
||||
this._sendEvent({
|
||||
name: 'file-uploaded',
|
||||
file: fileMetadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the external application that a file has been deleted.
|
||||
*
|
||||
* @param {string} fileId - The ID of the deleted file.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyFileDeleted(fileId) {
|
||||
this._sendEvent({
|
||||
name: 'file-deleted',
|
||||
fileId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the external application that the audio or video is being shared by a participant.
|
||||
*
|
||||
|
||||
2
modules/API/external/external_api.js
vendored
2
modules/API/external/external_api.js
vendored
@@ -133,8 +133,6 @@ const events = {
|
||||
'face-landmark-detected': 'faceLandmarkDetected',
|
||||
'feedback-submitted': 'feedbackSubmitted',
|
||||
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
|
||||
'file-deleted': 'fileDeleted',
|
||||
'file-uploaded': 'fileUploaded',
|
||||
'filmstrip-display-changed': 'filmstripDisplayChanged',
|
||||
'incoming-message': 'incomingMessage',
|
||||
'knocking-participant': 'knockingParticipant',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Logger from '@jitsi/logger';
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
@@ -166,7 +166,8 @@ export default class LargeVideoManager {
|
||||
|
||||
this.removePresenceLabel();
|
||||
|
||||
ReactDOM.unmountComponentAtNode(this._dominantSpeakerAvatarContainer);
|
||||
this._avatarRoot?.unmount();
|
||||
this._avatarRoot = null;
|
||||
|
||||
this.container.style.display = 'none';
|
||||
}
|
||||
@@ -518,14 +519,16 @@ export default class LargeVideoManager {
|
||||
* Updates the src of the dominant speaker avatar
|
||||
*/
|
||||
updateAvatar() {
|
||||
ReactDOM.render(
|
||||
if (!this._avatarRoot) {
|
||||
this._avatarRoot = createRoot(this._dominantSpeakerAvatarContainer);
|
||||
}
|
||||
this._avatarRoot.render(
|
||||
<Provider store = { APP.store }>
|
||||
<Avatar
|
||||
id = "dominantSpeakerAvatar"
|
||||
participantId = { this.id }
|
||||
size = { 200 } />
|
||||
</Provider>,
|
||||
this._dominantSpeakerAvatarContainer
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -559,15 +562,18 @@ export default class LargeVideoManager {
|
||||
const presenceLabelContainer = document.getElementById('remotePresenceMessage');
|
||||
|
||||
if (presenceLabelContainer) {
|
||||
ReactDOM.render(
|
||||
if (!this._presenceLabelRoot) {
|
||||
this._presenceLabelRoot = createRoot(presenceLabelContainer);
|
||||
}
|
||||
this._presenceLabelRoot.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<PresenceLabel
|
||||
participantID = { id }
|
||||
className = 'presence-label' />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
presenceLabelContainer);
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,11 +583,8 @@ export default class LargeVideoManager {
|
||||
* @returns {void}
|
||||
*/
|
||||
removePresenceLabel() {
|
||||
const presenceLabelContainer = document.getElementById('remotePresenceMessage');
|
||||
|
||||
if (presenceLabelContainer) {
|
||||
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
|
||||
}
|
||||
this._presenceLabelRoot?.unmount();
|
||||
this._presenceLabelRoot = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Logger from '@jitsi/logger';
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { browser } from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { FILMSTRIP_BREAKPOINT } from '../../../react/features/filmstrip/constants';
|
||||
@@ -659,7 +659,12 @@ export class VideoContainer extends LargeContainer {
|
||||
return;
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
const container = document.getElementById('largeVideoBackgroundContainer');
|
||||
|
||||
if (!this._backgroundRoot) {
|
||||
this._backgroundRoot = createRoot(container);
|
||||
}
|
||||
this._backgroundRoot.render(
|
||||
<LargeVideoBackground
|
||||
hidden = { this._hideBackground || this._isHidden }
|
||||
mirror = {
|
||||
@@ -669,8 +674,7 @@ export class VideoContainer extends LargeContainer {
|
||||
}
|
||||
orientationFit = { this._backgroundOrientation }
|
||||
videoElement = { this.video }
|
||||
videoTrack = { this.stream } />,
|
||||
document.getElementById('largeVideoBackgroundContainer')
|
||||
videoTrack = { this.stream } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './utils.any';
|
||||
@@ -29,3 +29,4 @@ export function isIpadMobileBrowser() {
|
||||
// @ts-ignore
|
||||
return isIosMobileBrowser() && Platform.isPad;
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { MIN_FILMSTRIP_RESIZE_WIDTH } from '../../filmstrip/constants';
|
||||
|
||||
/**
|
||||
* Detects if the current device has touch capability.
|
||||
* This includes smartphones, tablets, and laptops with touch screens.
|
||||
*
|
||||
* @returns {boolean} True if the device supports touch events.
|
||||
*/
|
||||
export function isTouchDevice(): boolean {
|
||||
// Check maxTouchPoints (most reliable for modern browsers)
|
||||
if ('maxTouchPoints' in navigator) {
|
||||
return navigator.maxTouchPoints > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if resize functionality should be enabled based on device capabilities
|
||||
* and screen size. On touch devices, resize is only enabled for larger screens.
|
||||
* On non-touch devices (desktop), resize is always enabled.
|
||||
*
|
||||
* @returns {boolean} True if resize functionality should be available to the user.
|
||||
*/
|
||||
export function shouldEnableResize(): boolean {
|
||||
const hasTouch = isTouchDevice();
|
||||
|
||||
// On non-touch devices (desktop), always enable resize
|
||||
if (!hasTouch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// On touch devices, only enable if screen is large enough.
|
||||
return window?.innerWidth >= MIN_FILMSTRIP_RESIZE_WIDTH;
|
||||
}
|
||||
|
||||
export * from './utils.any';
|
||||
@@ -60,6 +60,66 @@ const AVATAR_CHECKER_FUNCTIONS = [
|
||||
return null;
|
||||
}
|
||||
];
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
/**
|
||||
* Returns the list of active speakers that should be moved to the top of the sorted list of participants so that the
|
||||
* dominant speaker is visible always on the vertical filmstrip in stage layout.
|
||||
*
|
||||
* @param {Function | Object} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getActiveSpeakersToBeDisplayed(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
dominantSpeaker,
|
||||
fakeParticipants,
|
||||
sortedRemoteVirtualScreenshareParticipants,
|
||||
speakersList
|
||||
} = state['features/base/participants'];
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
let activeSpeakers = new Map(speakersList);
|
||||
|
||||
// Do not re-sort the active speakers if dominant speaker is currently visible.
|
||||
if (dominantSpeaker && visibleRemoteParticipants.has(dominantSpeaker)) {
|
||||
return activeSpeakers;
|
||||
}
|
||||
let availableSlotsForActiveSpeakers = visibleRemoteParticipants.size;
|
||||
|
||||
if (activeSpeakers.has(dominantSpeaker ?? '')) {
|
||||
activeSpeakers.delete(dominantSpeaker ?? '');
|
||||
}
|
||||
|
||||
// Add dominant speaker to the beginning of the list (not including self) since the active speaker list is always
|
||||
// alphabetically sorted.
|
||||
if (dominantSpeaker && dominantSpeaker !== getLocalParticipant(state)?.id) {
|
||||
const updatedSpeakers = Array.from(activeSpeakers);
|
||||
|
||||
updatedSpeakers.splice(0, 0, [ dominantSpeaker, getParticipantById(state, dominantSpeaker)?.name ?? '' ]);
|
||||
activeSpeakers = new Map(updatedSpeakers);
|
||||
}
|
||||
|
||||
// Remove screenshares from the count.
|
||||
if (sortedRemoteVirtualScreenshareParticipants) {
|
||||
availableSlotsForActiveSpeakers -= sortedRemoteVirtualScreenshareParticipants.size * 2;
|
||||
for (const screenshare of Array.from(sortedRemoteVirtualScreenshareParticipants.keys())) {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare as string);
|
||||
|
||||
activeSpeakers.delete(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove fake participants from the count.
|
||||
if (fakeParticipants) {
|
||||
availableSlotsForActiveSpeakers -= fakeParticipants.size;
|
||||
}
|
||||
const truncatedSpeakersList = Array.from(activeSpeakers).slice(0, availableSlotsForActiveSpeakers);
|
||||
|
||||
truncatedSpeakersList.sort((a: any, b: any) => a[1].localeCompare(b[1]));
|
||||
|
||||
return new Map(truncatedSpeakersList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
|
||||
@@ -68,7 +68,6 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
|
||||
];
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
activeSpeakers: new Set<string>(),
|
||||
dominantSpeaker: undefined,
|
||||
fakeParticipants: new Map(),
|
||||
local: undefined,
|
||||
@@ -83,10 +82,10 @@ const DEFAULT_STATE = {
|
||||
remoteVideoSources: new Set<string>(),
|
||||
sortedRemoteVirtualScreenshareParticipants: new Map(),
|
||||
sortedRemoteParticipants: new Map(),
|
||||
speakersList: new Map()
|
||||
};
|
||||
|
||||
export interface IParticipantsState {
|
||||
activeSpeakers: Set<string>;
|
||||
dominantSpeaker?: string;
|
||||
fakeParticipants: Map<string, IParticipant>;
|
||||
local?: ILocalParticipant;
|
||||
@@ -101,6 +100,7 @@ export interface IParticipantsState {
|
||||
remoteVideoSources: Set<string>;
|
||||
sortedRemoteParticipants: Map<string, string>;
|
||||
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
|
||||
speakersList: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,7 +157,22 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
const { participant } = action;
|
||||
const { id, previousSpeakers = [] } = participant;
|
||||
const { dominantSpeaker, local } = state;
|
||||
const activeSpeakers = new Set(previousSpeakers.filter((speakerId: string) => speakerId !== local?.id));
|
||||
const newSpeakers = [ id, ...previousSpeakers ];
|
||||
const sortedSpeakersList: Array<Array<string>> = [];
|
||||
|
||||
for (const speaker of newSpeakers) {
|
||||
if (speaker !== local?.id) {
|
||||
const remoteParticipant = state.remote.get(speaker);
|
||||
|
||||
remoteParticipant
|
||||
&& sortedSpeakersList.push(
|
||||
[ speaker, _getDisplayName(state, remoteParticipant?.name) ]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the remote speaker list sorted alphabetically.
|
||||
sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
// Only one dominant speaker is allowed.
|
||||
if (dominantSpeaker) {
|
||||
@@ -168,7 +183,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
return {
|
||||
...state,
|
||||
dominantSpeaker: id, // @ts-ignore
|
||||
activeSpeakers
|
||||
speakersList: new Map(sortedSpeakersList)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -423,7 +438,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
}
|
||||
|
||||
// Remove the participant from the list of speakers.
|
||||
state.activeSpeakers.delete(id);
|
||||
state.speakersList.has(id) && state.speakersList.delete(id);
|
||||
|
||||
if (pinnedParticipant === id) {
|
||||
state.pinnedParticipant = undefined;
|
||||
|
||||
@@ -107,12 +107,12 @@ const useStyles = makeStyles()(theme => {
|
||||
},
|
||||
|
||||
'input::-webkit-outer-spin-button, input::-webkit-inner-spin-button': {
|
||||
'-webkit-appearance': 'none',
|
||||
WebkitAppearance: 'none',
|
||||
margin: 0
|
||||
},
|
||||
|
||||
'input[type=number]': {
|
||||
'-moz-appearance': 'textfield'
|
||||
MozAppearance: 'textfield'
|
||||
},
|
||||
|
||||
icon: {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { connect, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { isTouchDevice, shouldEnableResize } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconInfo, IconMessage, IconShareDoc, IconSubtitles } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant, getRemoteParticipants, isPrivateChatEnabledSelf } from '../../../base/participants/functions';
|
||||
@@ -24,16 +23,7 @@ import {
|
||||
setUserChatWidth,
|
||||
toggleChat
|
||||
} from '../../actions.web';
|
||||
import {
|
||||
CHAT_DRAG_HANDLE_HEIGHT,
|
||||
CHAT_DRAG_HANDLE_OFFSET,
|
||||
CHAT_DRAG_HANDLE_WIDTH,
|
||||
CHAT_SIZE,
|
||||
CHAT_TOUCH_HANDLE_SIZE,
|
||||
ChatTabs,
|
||||
OPTION_GROUPCHAT,
|
||||
SMALL_WIDTH_THRESHOLD
|
||||
} from '../../constants';
|
||||
import { CHAT_SIZE, ChatTabs, OPTION_GROUPCHAT, SMALL_WIDTH_THRESHOLD } from '../../constants';
|
||||
import { getChatMaxSize, getFocusedTab, isChatDisabled } from '../../functions';
|
||||
import { IChatProps as AbstractProps } from '../../types';
|
||||
|
||||
@@ -114,12 +104,7 @@ interface IProps extends AbstractProps {
|
||||
_width: number;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<{
|
||||
_isResizing: boolean;
|
||||
isTouch: boolean;
|
||||
resizeEnabled: boolean;
|
||||
width: number;
|
||||
}>()((theme, { _isResizing, isTouch, resizeEnabled, width }) => {
|
||||
const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme, { _isResizing, width }) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.palette.chatBackground,
|
||||
@@ -130,15 +115,11 @@ const useStyles = makeStyles<{
|
||||
width: `${width}px`,
|
||||
zIndex: 300,
|
||||
|
||||
// On non-touch devices (desktop), show handle on hover
|
||||
// On touch devices, handle is always visible if resize is enabled
|
||||
...(!isTouch && {
|
||||
'&:hover, &:focus-within': {
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible'
|
||||
}
|
||||
'&:hover, &:focus-within': {
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible'
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
'@media (max-width: 580px)': {
|
||||
height: '100dvh',
|
||||
@@ -151,7 +132,7 @@ const useStyles = makeStyles<{
|
||||
|
||||
'*': {
|
||||
userSelect: 'text',
|
||||
'-webkit-user-select': 'text'
|
||||
WebkitUserSelect: 'text'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -202,23 +183,16 @@ const useStyles = makeStyles<{
|
||||
|
||||
dragHandleContainer: {
|
||||
height: '100%',
|
||||
// Touch devices need larger hit target but positioned to not take extra space
|
||||
width: isTouch ? `${CHAT_TOUCH_HANDLE_SIZE}px` : `${CHAT_DRAG_HANDLE_WIDTH}px`,
|
||||
width: '9px',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
cursor: 'col-resize',
|
||||
display: resizeEnabled ? 'flex' : 'none', // Hide if resize not enabled
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
// On touch devices, always visible if resize enabled. On desktop, hidden by default
|
||||
visibility: (isTouch && resizeEnabled) ? 'visible' : 'hidden',
|
||||
// Position touch handle centered on offset from edge, maintaining same gap as non-touch
|
||||
right: isTouch
|
||||
? `${CHAT_DRAG_HANDLE_OFFSET - Math.floor((CHAT_TOUCH_HANDLE_SIZE - CHAT_DRAG_HANDLE_WIDTH) / 2)}px`
|
||||
: `${CHAT_DRAG_HANDLE_OFFSET}px`,
|
||||
visibility: 'hidden',
|
||||
right: '4px',
|
||||
top: 0,
|
||||
// Prevent touch scrolling while dragging
|
||||
touchAction: 'none',
|
||||
|
||||
'&:hover': {
|
||||
'& .dragHandle': {
|
||||
@@ -236,15 +210,10 @@ const useStyles = makeStyles<{
|
||||
},
|
||||
|
||||
dragHandle: {
|
||||
// Keep the same visual appearance on all devices
|
||||
backgroundColor: theme.palette.icon02,
|
||||
height: `${CHAT_DRAG_HANDLE_HEIGHT}px`,
|
||||
width: `${CHAT_DRAG_HANDLE_WIDTH / 3}px`,
|
||||
borderRadius: '1px',
|
||||
// Make more visible when actively shown
|
||||
...(isTouch && resizeEnabled && {
|
||||
backgroundColor: theme.palette.icon01
|
||||
})
|
||||
height: '100px',
|
||||
width: '3px',
|
||||
borderRadius: '1px'
|
||||
},
|
||||
|
||||
privateMessageRecipientsList: {
|
||||
@@ -277,10 +246,7 @@ const Chat = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Detect touch capability and screen size for resize functionality
|
||||
const isTouch = isTouchDevice();
|
||||
const resizeEnabled = shouldEnableResize();
|
||||
const { classes, cx } = useStyles({ _isResizing, width: _width, isTouch, resizeEnabled });
|
||||
const { classes, cx } = useStyles({ _isResizing, width: _width });
|
||||
const [ isMouseDown, setIsMouseDown ] = useState(false);
|
||||
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
|
||||
const [ dragChatWidth, setDragChatWidth ] = useState<number | null>(null);
|
||||
@@ -316,21 +282,16 @@ const Chat = ({
|
||||
}, [ participants, defaultRemoteDisplayName, t, notifyTimestamp ]);
|
||||
|
||||
/**
|
||||
* Handles pointer down on the drag handle.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
* Handles mouse down on the drag handle.
|
||||
*
|
||||
* @param {React.PointerEvent} e - The pointer down event.
|
||||
* @param {MouseEvent} e - The mouse down event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragHandlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||
const onDragHandleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Capture the pointer to ensure we receive all pointer events
|
||||
// even if the pointer moves outside the element
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
|
||||
// Store the initial pointer position and chat width
|
||||
// Store the initial mouse position and chat width
|
||||
setIsMouseDown(true);
|
||||
setMousePosition(e.clientX);
|
||||
setDragChatWidth(_width);
|
||||
@@ -338,7 +299,7 @@ const Chat = ({
|
||||
// Indicate that resizing is in progress
|
||||
dispatch(setChatIsResizing(true));
|
||||
|
||||
// Add visual feedback that we're dragging (cursor for mouse, not visible on touch)
|
||||
// Add visual feedback that we're dragging
|
||||
document.body.style.cursor = 'col-resize';
|
||||
|
||||
// Disable text selection during resize
|
||||
@@ -346,12 +307,11 @@ const Chat = ({
|
||||
}, [ _width, dispatch ]);
|
||||
|
||||
/**
|
||||
* Drag handle pointer up handler.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
* Drag handle mouse up handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragPointerUp = useCallback(() => {
|
||||
const onDragMouseUp = useCallback(() => {
|
||||
if (isMouseDown) {
|
||||
setIsMouseDown(false);
|
||||
dispatch(setChatIsResizing(false));
|
||||
@@ -363,13 +323,12 @@ const Chat = ({
|
||||
}, [ isMouseDown, dispatch ]);
|
||||
|
||||
/**
|
||||
* Handles drag handle pointer move.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
* Handles drag handle mouse move.
|
||||
*
|
||||
* @param {PointerEvent} e - The pointermove event.
|
||||
* @param {MouseEvent} e - The mousemove event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onChatResize = useCallback(throttle((e: PointerEvent) => {
|
||||
const onChatResize = useCallback(throttle((e: MouseEvent) => {
|
||||
if (isMouseDown && mousePosition !== null && dragChatWidth !== null) {
|
||||
// For chat panel resizing on the left edge:
|
||||
// - Dragging left (decreasing X coordinate) should make the panel wider
|
||||
@@ -393,14 +352,14 @@ const Chat = ({
|
||||
|
||||
// Set up event listeners when component mounts
|
||||
useEffect(() => {
|
||||
document.addEventListener('pointerup', onDragPointerUp);
|
||||
document.addEventListener('pointermove', onChatResize);
|
||||
document.addEventListener('mouseup', onDragMouseUp);
|
||||
document.addEventListener('mousemove', onChatResize);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('pointerup', onDragPointerUp);
|
||||
document.removeEventListener('pointermove', onChatResize);
|
||||
document.removeEventListener('mouseup', onDragMouseUp);
|
||||
document.removeEventListener('mousemove', onChatResize);
|
||||
};
|
||||
}, [ onDragPointerUp, onChatResize ]);
|
||||
}, [ onDragMouseUp, onChatResize ]);
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
@@ -641,7 +600,7 @@ const Chat = ({
|
||||
(isMouseDown || _isResizing) && 'visible',
|
||||
'dragHandleContainer'
|
||||
) }
|
||||
onPointerDown = { onDragHandlePointerDown }>
|
||||
onMouseDown = { onDragHandleMouseDown }>
|
||||
<div className = { cx(classes.dragHandle, 'dragHandle') } />
|
||||
</div>
|
||||
</div> : null
|
||||
|
||||
@@ -33,23 +33,6 @@ export const MESSAGE_TYPE_REMOTE = 'remote';
|
||||
|
||||
export const SMALL_WIDTH_THRESHOLD = 580;
|
||||
|
||||
/**
|
||||
* Drag handle dimensions for resizable chat.
|
||||
*/
|
||||
export const CHAT_DRAG_HANDLE_WIDTH = 9;
|
||||
export const CHAT_DRAG_HANDLE_HEIGHT = 100;
|
||||
|
||||
/**
|
||||
* Touch target size for chat drag handle on touch devices.
|
||||
* Provides adequate hit area (44px) for comfortable tapping.
|
||||
*/
|
||||
export const CHAT_TOUCH_HANDLE_SIZE = 44;
|
||||
|
||||
/**
|
||||
* Offset from edge for positioning the chat drag handle.
|
||||
*/
|
||||
export const CHAT_DRAG_HANDLE_OFFSET = 4;
|
||||
|
||||
|
||||
/**
|
||||
* Lobby message type.
|
||||
|
||||
@@ -72,9 +72,6 @@ StateListenerRegistry.register(
|
||||
type: _FILE_REMOVED,
|
||||
fileId
|
||||
});
|
||||
|
||||
// Notify external API about file deletion (for remote participants)
|
||||
APP.API.notifyFileDeleted(fileId);
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
|
||||
@@ -86,11 +83,6 @@ StateListenerRegistry.register(
|
||||
files,
|
||||
localParticipantId: localParticipant?.id
|
||||
});
|
||||
|
||||
// Notify external API about existing files (for participants joining late)
|
||||
Object.values(files).forEach((file: IFileMetadata) => {
|
||||
APP.API.notifyFileUploaded(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -160,8 +152,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
privateMessage: false,
|
||||
timestamp: file.timestamp
|
||||
}));
|
||||
|
||||
APP.API.notifyFileUploaded(file);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -188,9 +178,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
fileId
|
||||
});
|
||||
|
||||
// Notify external API about file deletion (for the local participant who deleted it)
|
||||
APP.API.notifyFileDeleted(fileId);
|
||||
|
||||
const { fileSharing } = state['features/base/config'];
|
||||
const sessionId = conference.getMeetingUniqueId();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { withStyles } from 'tss-react/mui';
|
||||
import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { isMobileBrowser, isTouchDevice, shouldEnableResize } from '../../../base/environment/utils';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
@@ -32,17 +32,12 @@ import {
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
DEFAULT_FILMSTRIP_WIDTH,
|
||||
DRAG_HANDLE_HEIGHT,
|
||||
DRAG_HANDLE_TOP_PANEL_HEIGHT,
|
||||
DRAG_HANDLE_TOP_PANEL_WIDTH,
|
||||
DRAG_HANDLE_WIDTH,
|
||||
FILMSTRIP_TYPE,
|
||||
MIN_STAGE_VIEW_HEIGHT,
|
||||
MIN_STAGE_VIEW_WIDTH,
|
||||
TILE_HORIZONTAL_MARGIN,
|
||||
TILE_VERTICAL_MARGIN,
|
||||
TOP_FILMSTRIP_HEIGHT,
|
||||
TOUCH_DRAG_HANDLE_PADDING
|
||||
TOP_FILMSTRIP_HEIGHT
|
||||
} from '../../constants';
|
||||
import {
|
||||
getVerticalViewMaxWidth,
|
||||
@@ -57,21 +52,6 @@ import ThumbnailWrapper from './ThumbnailWrapper';
|
||||
|
||||
|
||||
const BACKGROUND_COLOR = 'rgba(51, 51, 51, .5)';
|
||||
const TOUCH_DEVICE_PADDING = {
|
||||
paddingLeft: `${TOUCH_DRAG_HANDLE_PADDING}px`,
|
||||
paddingRight: `${TOUCH_DRAG_HANDLE_PADDING}px`,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
};
|
||||
const TOUCH_DEVICE_TOP_PANEL_PADDING = {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
paddingTop: `${TOUCH_DRAG_HANDLE_PADDING}px`,
|
||||
paddingBottom: `${TOUCH_DRAG_HANDLE_PADDING}px`
|
||||
};
|
||||
const NON_TOUCH_DEVICE_PANEL = {
|
||||
pading: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
@@ -81,14 +61,6 @@ const NON_TOUCH_DEVICE_PANEL = {
|
||||
* @returns {Object}
|
||||
*/
|
||||
function styles(theme: Theme, props: IProps) {
|
||||
const { _topPanelFilmstrip: isTopPanel } = props;
|
||||
|
||||
const _isTouchDevice = isTouchDevice();
|
||||
const resizeEnabled = shouldEnableResize();
|
||||
const handlePaddding = _isTouchDevice
|
||||
? (isTopPanel ? TOUCH_DEVICE_TOP_PANEL_PADDING : TOUCH_DEVICE_PADDING)
|
||||
: NON_TOUCH_DEVICE_PANEL;
|
||||
|
||||
const result = {
|
||||
toggleFilmstripContainer: {
|
||||
display: 'flex',
|
||||
@@ -122,7 +94,7 @@ function styles(theme: Theme, props: IProps) {
|
||||
margin: 0,
|
||||
border: 'none',
|
||||
|
||||
'-webkit-appearance': 'none',
|
||||
WebkitAppearance: 'none' as const,
|
||||
|
||||
'& svg': {
|
||||
fill: theme.palette.icon01
|
||||
@@ -150,27 +122,23 @@ function styles(theme: Theme, props: IProps) {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
|
||||
// On touch devices, handle is always visible via base styles, so no hover needed.
|
||||
// On desktop, show handle on hover/focus.
|
||||
...(!_isTouchDevice && {
|
||||
'&:hover, &:focus-within': {
|
||||
'& .resizable-filmstrip': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
'&:hover, &:focus-within': {
|
||||
'& .resizable-filmstrip': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
|
||||
'& .filmstrip-hover': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
'& .filmstrip-hover': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
|
||||
'& .toggleFilmstripContainer': {
|
||||
opacity: 1
|
||||
},
|
||||
'& .toggleFilmstripContainer': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible' as const
|
||||
}
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible' as const
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
'.horizontal-filmstrip &.hidden': {
|
||||
bottom: '-50px',
|
||||
@@ -219,22 +187,14 @@ function styles(theme: Theme, props: IProps) {
|
||||
|
||||
dragHandleContainer: {
|
||||
height: '100%',
|
||||
width: `${DRAG_HANDLE_WIDTH}px`,
|
||||
width: '9px',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative' as const,
|
||||
cursor: 'col-resize',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
// On touch devices, always visible if resize enabled. On desktop, hidden by default
|
||||
visibility: (_isTouchDevice && resizeEnabled) ? 'visible' as const : 'hidden' as const,
|
||||
marginLeft: 0,
|
||||
marginTop: 0,
|
||||
// Touch devices get padding for easier tapping
|
||||
// Vertical filmstrip: left/right padding. Top panel: top/bottom padding.
|
||||
...handlePaddding,
|
||||
// Prevent touch scrolling while dragging
|
||||
touchAction: 'none',
|
||||
visibility: 'hidden' as const,
|
||||
|
||||
'&:hover': {
|
||||
'& .dragHandle': {
|
||||
@@ -253,21 +213,20 @@ function styles(theme: Theme, props: IProps) {
|
||||
'&.top-panel': {
|
||||
order: 2,
|
||||
width: '100%',
|
||||
height: `${DRAG_HANDLE_WIDTH}px`,
|
||||
height: '9px',
|
||||
cursor: 'row-resize',
|
||||
|
||||
'& .dragHandle': {
|
||||
height: `${DRAG_HANDLE_TOP_PANEL_HEIGHT}px`,
|
||||
width: `${DRAG_HANDLE_TOP_PANEL_WIDTH}px`
|
||||
height: '3px',
|
||||
width: '100px'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
dragHandle: {
|
||||
// Keep the same visual appearance on all devices
|
||||
backgroundColor: theme.palette.filmstripDragHandle,
|
||||
height: `${DRAG_HANDLE_HEIGHT}px`,
|
||||
width: `${DRAG_HANDLE_WIDTH / 3}px`,
|
||||
height: '100px',
|
||||
width: '3px',
|
||||
borderRadius: '1px'
|
||||
}
|
||||
};
|
||||
@@ -354,11 +313,6 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_isToolboxVisible: Boolean;
|
||||
|
||||
/**
|
||||
* Whether the device has touch capability.
|
||||
*/
|
||||
_isTouchDevice?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the current layout is vertical filmstrip.
|
||||
*/
|
||||
@@ -404,11 +358,6 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_resizableFilmstrip: boolean;
|
||||
|
||||
/**
|
||||
* Whether resize functionality should be enabled based on device and screen size.
|
||||
*/
|
||||
_resizeEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* The number of rows in tile view.
|
||||
*/
|
||||
@@ -542,10 +491,8 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
this._onGridItemsRendered = this._onGridItemsRendered.bind(this);
|
||||
this._onListItemsRendered = this._onListItemsRendered.bind(this);
|
||||
this._onToggleButtonTouch = this._onToggleButtonTouch.bind(this);
|
||||
this._onDragHandlePointerDown = this._onDragHandlePointerDown.bind(this);
|
||||
this._onDragHandleClick = this._onDragHandleClick.bind(this);
|
||||
this._onDragHandleTouchStart = this._onDragHandleTouchStart.bind(this);
|
||||
this._onDragPointerUp = this._onDragPointerUp.bind(this);
|
||||
this._onDragHandleMouseDown = this._onDragHandleMouseDown.bind(this);
|
||||
this._onDragMouseUp = this._onDragMouseUp.bind(this);
|
||||
this._onFilmstripResize = this._onFilmstripResize.bind(this);
|
||||
|
||||
this._throttledResize = throttle(
|
||||
@@ -569,10 +516,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
handler: this._onShortcutToggleFilmstrip
|
||||
}));
|
||||
|
||||
document.addEventListener('pointerup', this._onDragPointerUp);
|
||||
document.addEventListener('mouseup', this._onDragMouseUp);
|
||||
|
||||
// @ts-ignore
|
||||
document.addEventListener('pointermove', this._throttledResize);
|
||||
document.addEventListener('mousemove', this._throttledResize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -583,10 +530,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
override componentWillUnmount() {
|
||||
this.props.dispatch(unregisterShortcut('F'));
|
||||
|
||||
document.removeEventListener('pointerup', this._onDragPointerUp);
|
||||
document.removeEventListener('mouseup', this._onDragMouseUp);
|
||||
|
||||
// @ts-ignore
|
||||
document.removeEventListener('pointermove', this._throttledResize);
|
||||
document.removeEventListener('mousemove', this._throttledResize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -731,9 +678,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
(isMouseDown || _alwaysShowResizeBar) && 'visible',
|
||||
_topPanelFilmstrip && 'top-panel')
|
||||
}
|
||||
onClick = { this._onDragHandleClick }
|
||||
onPointerDown = { this._onDragHandlePointerDown }
|
||||
onTouchStart = { this._onDragHandleTouchStart }>
|
||||
onMouseDown = { this._onDragHandleMouseDown }>
|
||||
<div className = { clsx(classes.dragHandle, 'dragHandle') } />
|
||||
</div>
|
||||
{filmstrip}
|
||||
@@ -746,23 +691,14 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles pointer down on the drag handle.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
* Handles mouse down on the drag handle.
|
||||
*
|
||||
* @param {React.PointerEvent} e - The pointer down event.
|
||||
* @param {MouseEvent} e - The mouse down event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragHandlePointerDown(e: React.PointerEvent) {
|
||||
_onDragHandleMouseDown(e: React.MouseEvent) {
|
||||
const { _topPanelFilmstrip, _topPanelHeight, _verticalFilmstripWidth } = this.props;
|
||||
|
||||
// Prevent toolbar from appearing and stop event propagation
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Capture the pointer to ensure we receive all pointer events
|
||||
// even if the pointer moves outside the element
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
|
||||
this.setState({
|
||||
isMouseDown: true,
|
||||
mousePosition: _topPanelFilmstrip ? e.clientY : e.clientX,
|
||||
@@ -773,33 +709,11 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents click events on drag handle from triggering toolbar.
|
||||
*
|
||||
* @param {React.MouseEvent} e - The click event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragHandleClick(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents touch start events on drag handle from triggering toolbar.
|
||||
*
|
||||
* @param {React.TouchEvent} e - The touch start event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragHandleTouchStart(e: React.TouchEvent) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag handle pointer up handler.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
* Drag handle mouse up handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragPointerUp() {
|
||||
_onDragMouseUp() {
|
||||
if (this.state.isMouseDown) {
|
||||
this.setState({
|
||||
isMouseDown: false
|
||||
@@ -809,13 +723,12 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles drag handle pointer move.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
* Handles drag handle mouse move.
|
||||
*
|
||||
* @param {PointerEvent} e - The pointermove event.
|
||||
* @param {MouseEvent} e - The mousemove event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFilmstripResize(e: PointerEvent) {
|
||||
_onFilmstripResize(e: React.MouseEvent) {
|
||||
if (this.state.isMouseDown) {
|
||||
const {
|
||||
dispatch,
|
||||
@@ -1250,4 +1163,4 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(withStyles(Filmstrip, styles)));
|
||||
export default withStyles(translate(connect(_mapStateToProps)(Filmstrip)), styles);
|
||||
|
||||
@@ -269,19 +269,6 @@ export const MIN_STAGE_VIEW_HEIGHT = 700;
|
||||
*/
|
||||
export const MIN_STAGE_VIEW_WIDTH = 800;
|
||||
|
||||
/**
|
||||
* Drag handle dimensions for resizable filmstrip.
|
||||
*/
|
||||
export const DRAG_HANDLE_WIDTH = 9;
|
||||
export const DRAG_HANDLE_HEIGHT = 100;
|
||||
export const DRAG_HANDLE_TOP_PANEL_HEIGHT = 3;
|
||||
export const DRAG_HANDLE_TOP_PANEL_WIDTH = 100;
|
||||
|
||||
/**
|
||||
* Touch padding added to each side of drag handle for easier tapping on touch devices.
|
||||
*/
|
||||
export const TOUCH_DRAG_HANDLE_PADDING = 6;
|
||||
|
||||
/**
|
||||
* Horizontal margin used for the vertical filmstrip.
|
||||
*/
|
||||
@@ -311,9 +298,3 @@ export const MAX_ACTIVE_PARTICIPANTS = 6;
|
||||
* Top filmstrip default height.
|
||||
*/
|
||||
export const TOP_FILMSTRIP_HEIGHT = 180;
|
||||
|
||||
/**
|
||||
* Minimum screen width needed for functional filmstrip resizing.
|
||||
* Calculated as stage minimum + filmstrip minimum (800px + 120px = 920px).
|
||||
*/
|
||||
export const MIN_FILMSTRIP_RESIZE_WIDTH = MIN_STAGE_VIEW_WIDTH + DEFAULT_FILMSTRIP_WIDTH;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import { getParticipantById, getVirtualScreenshareParticipantOwnerId } from '../base/participants/functions';
|
||||
import {
|
||||
getActiveSpeakersToBeDisplayed,
|
||||
getVirtualScreenshareParticipantOwnerId
|
||||
} from '../base/participants/functions';
|
||||
|
||||
import { setRemoteParticipants } from './actions';
|
||||
import { isFilmstripScrollVisible } from './functions';
|
||||
@@ -30,66 +33,44 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
|
||||
}
|
||||
|
||||
const {
|
||||
activeSpeakers,
|
||||
dominantSpeaker,
|
||||
fakeParticipants,
|
||||
sortedRemoteParticipants
|
||||
} = state['features/base/participants'];
|
||||
const config = state['features/base/config'];
|
||||
const defaultRemoteDisplayName = config?.defaultRemoteDisplayName ?? 'Fellow Jitster';
|
||||
const dominant = dominantSpeaker ? getParticipantById(state, dominantSpeaker) : undefined;
|
||||
let dominantSpeakerSlot = 0;
|
||||
const previousSpeakers = new Set(activeSpeakers);
|
||||
const remoteParticipants = new Map(sortedRemoteParticipants);
|
||||
const screenShareParticipants = sortedRemoteVirtualScreenshareParticipants
|
||||
? [ ...sortedRemoteVirtualScreenshareParticipants.keys() ] : [];
|
||||
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
|
||||
const speakers = new Array<string>();
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
const speakers = getActiveSpeakersToBeDisplayed(state);
|
||||
|
||||
for (const screenshare of screenShareParticipants) {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
|
||||
|
||||
remoteParticipants.delete(ownerId);
|
||||
remoteParticipants.delete(screenshare);
|
||||
speakers.delete(ownerId);
|
||||
}
|
||||
|
||||
for (const sharedVideo of sharedVideos) {
|
||||
remoteParticipants.delete(sharedVideo);
|
||||
}
|
||||
for (const speaker of speakers.keys()) {
|
||||
remoteParticipants.delete(speaker);
|
||||
}
|
||||
|
||||
// Always update the order of the thubmnails.
|
||||
const participantsWithScreenShare = screenShareParticipants.reduce<string[]>((acc, screenshare) => {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
|
||||
|
||||
acc.push(ownerId);
|
||||
acc.push(screenshare);
|
||||
remoteParticipants.delete(ownerId);
|
||||
remoteParticipants.delete(screenshare);
|
||||
previousSpeakers.delete(ownerId);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
for (const sharedVideo of sharedVideos) {
|
||||
remoteParticipants.delete(sharedVideo);
|
||||
}
|
||||
|
||||
if (dominant && !dominant.local && participantsWithScreenShare.indexOf(dominant.id) === -1) {
|
||||
dominantSpeakerSlot = 1;
|
||||
remoteParticipants.delete(dominant.id);
|
||||
speakers.push(dominant.id);
|
||||
}
|
||||
|
||||
// Find the number of slots available for speakers.
|
||||
const slotsForSpeakers
|
||||
= visibleRemoteParticipants.size - (screenShareParticipants.length * 2) - sharedVideos.length - dominantSpeakerSlot;
|
||||
|
||||
// Construct the list of speakers to be shown.
|
||||
if (slotsForSpeakers > 0) {
|
||||
Array.from(previousSpeakers).slice(0, slotsForSpeakers).forEach((speakerId: string) => {
|
||||
speakers.push(speakerId);
|
||||
remoteParticipants.delete(speakerId);
|
||||
});
|
||||
speakers.sort((a: string, b: string) => {
|
||||
return (getParticipantById(state, a)?.name ?? defaultRemoteDisplayName)
|
||||
.localeCompare(getParticipantById(state, b)?.name ?? defaultRemoteDisplayName);
|
||||
});
|
||||
}
|
||||
|
||||
// Always update the order of the thumbnails.
|
||||
reorderedParticipants = [
|
||||
...participantsWithScreenShare,
|
||||
...sharedVideos,
|
||||
...speakers,
|
||||
...Array.from(speakers.keys()),
|
||||
...Array.from(remoteParticipants.keys())
|
||||
];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Theme } from '@mui/material/styles';
|
||||
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { isTouchDevice, shouldEnableResize } from '../base/environment/utils';
|
||||
import { isMobileBrowser } from '../base/environment/utils';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
DEFAULT_LOCAL_TILE_ASPECT_RATIO,
|
||||
DISPLAY_AVATAR,
|
||||
DISPLAY_VIDEO,
|
||||
DRAG_HANDLE_WIDTH,
|
||||
FILMSTRIP_GRID_BREAKPOINT,
|
||||
FILMSTRIP_TYPE,
|
||||
INDICATORS_TOOLTIP_POSITION,
|
||||
@@ -46,7 +45,6 @@ import {
|
||||
TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
|
||||
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
|
||||
TILE_VIEW_GRID_VERTICAL_MARGIN,
|
||||
TOUCH_DRAG_HANDLE_PADDING,
|
||||
VERTICAL_VIEW_HORIZONTAL_MARGIN
|
||||
} from './constants';
|
||||
|
||||
@@ -623,7 +621,6 @@ export function getIndicatorsTooltipPosition(thumbnailType?: string) {
|
||||
|
||||
/**
|
||||
* Returns whether or not the filmstrip is resizable.
|
||||
* On touch devices, resize is only enabled for larger screens (tablets, not phones).
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean}
|
||||
@@ -632,7 +629,7 @@ export function isFilmstripResizable(state: IReduxState) {
|
||||
const { filmstrip } = state['features/base/config'];
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
|
||||
return !filmstrip?.disableResizable && shouldEnableResize()
|
||||
return !filmstrip?.disableResizable && !isMobileBrowser()
|
||||
&& (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW || _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
|
||||
}
|
||||
|
||||
@@ -665,13 +662,8 @@ export function getVerticalViewMaxWidth(state: IReduxState) {
|
||||
|
||||
// Adding 4px for the border-right and margin-right.
|
||||
// On non-resizable filmstrip add 4px for the left margin and border.
|
||||
// Also adding 7px for the scrollbar.
|
||||
// Drag handle: DRAG_HANDLE_WIDTH + padding (TOUCH_DRAG_HANDLE_PADDING on each side for touch)
|
||||
const dragHandleWidth = isTouchDevice()
|
||||
? DRAG_HANDLE_WIDTH + (TOUCH_DRAG_HANDLE_PADDING * 2)
|
||||
: DRAG_HANDLE_WIDTH;
|
||||
|
||||
maxWidth += (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? dragHandleWidth : 4);
|
||||
// Also adding 7px for the scrollbar. Also adding 9px for the drag handle.
|
||||
maxWidth += (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? 9 : 4);
|
||||
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
|
||||
fontWeight: 600,
|
||||
height: '100%',
|
||||
|
||||
[[ '& > *:first-child', '& > *:last-child' ] as any]: {
|
||||
[[ '& > *:first-of-type', '& > *:last-of-type' ] as any]: {
|
||||
flexShrink: 0
|
||||
},
|
||||
|
||||
@@ -116,11 +116,11 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
|
||||
antiCollapse: {
|
||||
fontSize: 0,
|
||||
|
||||
'&:first-child': {
|
||||
'&:first-of-type': {
|
||||
display: 'none'
|
||||
},
|
||||
|
||||
'&:first-child + *': {
|
||||
'&:first-of-type + *': {
|
||||
marginTop: 0
|
||||
}
|
||||
},
|
||||
|
||||
@@ -86,7 +86,7 @@ const useStyles = makeStyles()(theme => {
|
||||
slider: {
|
||||
// Use an additional class here to override global CSS specificity
|
||||
'&.custom-slider': {
|
||||
'-webkit-appearance': 'none',
|
||||
WebkitAppearance: 'none',
|
||||
background: 'transparent',
|
||||
height,
|
||||
left: 0,
|
||||
@@ -104,11 +104,11 @@ const useStyles = makeStyles()(theme => {
|
||||
},
|
||||
|
||||
'&::-webkit-slider-runnable-track': {
|
||||
'-webkit-appearance': 'none',
|
||||
WebkitAppearance: 'none',
|
||||
...inputTrack
|
||||
},
|
||||
'&::-webkit-slider-thumb': {
|
||||
'-webkit-appearance': 'none',
|
||||
WebkitAppearance: 'none',
|
||||
position: 'relative',
|
||||
top: -6,
|
||||
...inputThumb
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { App } from './features/app/components/App.web';
|
||||
import { getLogger } from './features/base/logging/functions';
|
||||
@@ -72,14 +72,17 @@ globalNS.entryPoints = {
|
||||
WHITEBOARD: WhiteboardApp
|
||||
};
|
||||
|
||||
const _roots = new Map();
|
||||
|
||||
globalNS.renderEntryPoint = ({
|
||||
Component,
|
||||
props = {},
|
||||
elementId = 'react'
|
||||
}) => {
|
||||
/* eslint-disable-next-line react/no-deprecated */
|
||||
ReactDOM.render(
|
||||
<Component { ...props } />,
|
||||
document.getElementById(elementId)
|
||||
);
|
||||
const element = document.getElementById(elementId);
|
||||
|
||||
if (!_roots.has(elementId)) {
|
||||
_roots.set(elementId, createRoot(element));
|
||||
}
|
||||
_roots.get(elementId).render(<Component { ...props } />);
|
||||
};
|
||||
|
||||
@@ -35,8 +35,6 @@ export const P1 = 'p1';
|
||||
export const P2 = 'p2';
|
||||
export const P3 = 'p3';
|
||||
export const P4 = 'p4';
|
||||
export const P5 = 'p5';
|
||||
export const P6 = 'p6';
|
||||
|
||||
/**
|
||||
* Participant.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { P1, P2, P3, P4, P5, P6, Participant } from './Participant';
|
||||
import { P1, P2, P3, P4, Participant } from './Participant';
|
||||
import { config } from './TestsConfig';
|
||||
import { IJoinOptions, IParticipantOptions } from './types';
|
||||
|
||||
@@ -122,50 +122,6 @@ export async function ensureFourParticipants(options?: IJoinOptions): Promise<vo
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are six participants.
|
||||
*
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureSixParticipants(options?: IJoinOptions): Promise<void> {
|
||||
await ensureOneParticipant(options);
|
||||
|
||||
// Join participants in batches
|
||||
await Promise.all([
|
||||
joinParticipant({ name: P2 }, options),
|
||||
joinParticipant({ name: P3 }, options),
|
||||
joinParticipant({ name: P4 }, options)
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
joinParticipant({ name: P5 }, options),
|
||||
joinParticipant({ name: P6 }, options)
|
||||
]);
|
||||
|
||||
if (options?.skipInMeetingChecks) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
ctx.p1.waitForIceConnected(),
|
||||
ctx.p2.waitForIceConnected(),
|
||||
ctx.p3.waitForIceConnected(),
|
||||
ctx.p4.waitForIceConnected(),
|
||||
ctx.p5.waitForIceConnected(),
|
||||
ctx.p6.waitForIceConnected()
|
||||
]);
|
||||
await Promise.all([
|
||||
ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
|
||||
ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)),
|
||||
ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)),
|
||||
ctx.p4.waitForSendReceiveData().then(() => ctx.p4.waitForRemoteStreams(1)),
|
||||
ctx.p5.waitForSendReceiveData().then(() => ctx.p5.waitForRemoteStreams(1)),
|
||||
ctx.p6.waitForSendReceiveData().then(() => ctx.p6.waitForRemoteStreams(1))
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that there are two participants.
|
||||
*
|
||||
@@ -288,16 +244,10 @@ export async function checkForScreensharingTile(sharer: Participant, observer: P
|
||||
}
|
||||
|
||||
/**
|
||||
* Hangs up all participants (p1, p2, p3, p4, p5, and p6)
|
||||
* Hangs up all participants (p1, p2, p3 and p4)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function hangupAllParticipants() {
|
||||
return Promise.all([
|
||||
ctx.p1?.hangup(),
|
||||
ctx.p2?.hangup(),
|
||||
ctx.p3?.hangup(),
|
||||
ctx.p4?.hangup(),
|
||||
ctx.p5?.hangup(),
|
||||
ctx.p6?.hangup()
|
||||
].map(p => p ?? Promise.resolve()));
|
||||
return Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup(), ctx.p3?.hangup(), ctx.p4?.hangup() ]
|
||||
.map(p => p ?? Promise.resolve()));
|
||||
}
|
||||
|
||||
@@ -7,16 +7,13 @@ import { IToken, ITokenOptions } from './token';
|
||||
|
||||
export type IContext = {
|
||||
/**
|
||||
* The up-to-six browser instances provided by the framework. These can be initialized using
|
||||
* The up-to-four browser instances provided by the framework. These can be initialized using
|
||||
* ensureOneParticipant, ensureTwoParticipants, etc. from participants.ts.
|
||||
**/
|
||||
p1: Participant;
|
||||
p2: Participant;
|
||||
p3: Participant;
|
||||
p4: Participant;
|
||||
p5: Participant;
|
||||
p6: Participant;
|
||||
|
||||
/** A room name automatically generated by the framework for convenience. */
|
||||
roomName: string;
|
||||
/**
|
||||
@@ -42,7 +39,7 @@ export type IParticipantOptions = {
|
||||
/** Whether it should use the iFrame API. */
|
||||
iFrameApi?: boolean;
|
||||
/** Determines the browser instance to use. */
|
||||
name: 'p1' | 'p2' | 'p3' | 'p4' | 'p5' | 'p6';
|
||||
name: 'p1' | 'p2' | 'p3' | 'p4';
|
||||
/** An optional token to use. */
|
||||
token?: IToken;
|
||||
};
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import {
|
||||
checkForScreensharingTile,
|
||||
ensureSixParticipants,
|
||||
ensureThreeParticipants,
|
||||
hangupAllParticipants
|
||||
} from '../../helpers/participants';
|
||||
import { ensureThreeParticipants } from '../../helpers/participants';
|
||||
import { muteAudioAndCheck } from '../helpers/mute';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4', 'p5', 'p6' ]
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Active speaker', () => {
|
||||
it('testActiveSpeaker', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await muteAudioAndCheck(p1, p2);
|
||||
@@ -34,306 +30,6 @@ describe('Active speaker', () => {
|
||||
await assertOneDominantSpeaker(p1);
|
||||
await assertOneDominantSpeaker(p2);
|
||||
await assertOneDominantSpeaker(p3);
|
||||
|
||||
await hangupAllParticipants();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that the dominant speaker appears in the filmstrip in stage view
|
||||
* even when alphabetically last with limited visible slots.
|
||||
* This tests the fix for the bug where dominant speakers at the bottom of
|
||||
* the alphabetically sorted list would not appear when slots were limited.
|
||||
*
|
||||
* Note: This test verifies filmstrip ordering via Redux state
|
||||
* (visibleRemoteParticipants), not large video behavior.
|
||||
*/
|
||||
it.skip('testDominantSpeakerInFilmstripWithLimitedSlots', async () => {
|
||||
await ensureSixParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
const { p1, p2, p3, p4, p5, p6 } = ctx;
|
||||
|
||||
// Resize p1's window to limit filmstrip slots to 2-3 tiles
|
||||
// This creates the condition where not all participants fit in the filmstrip
|
||||
await p1.driver.setWindowSize(1024, 600);
|
||||
await p1.driver.pause(1000); // Wait for layout to adjust
|
||||
|
||||
// Set display names to create alphabetical ordering
|
||||
// Names chosen so p6 ("Zoe") is alphabetically last: Alice, Bob, Charlie, Eve, Frank, Zoe
|
||||
await setAlphabeticalDisplayNames(p1, p2, p3, p4, p5, p6);
|
||||
|
||||
// Test with multiple speakers: Eve (p4), Frank (p5), and Zoe (p6)
|
||||
// This verifies the fix works for different alphabetical positions
|
||||
const speakersToTest = [
|
||||
{ participant: p4, name: 'Eve' },
|
||||
{ participant: p5, name: 'Frank' },
|
||||
{ participant: p6, name: 'Zoe' }
|
||||
];
|
||||
|
||||
for (const { participant, name } of speakersToTest) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await p1.log(`Testing ${name} (${participantId}) as dominant speaker`);
|
||||
|
||||
// Make this participant the dominant speaker by unmuting
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Wait for the dominant speaker to be detected
|
||||
await waitForDominantSpeaker(p1, participantId, name);
|
||||
|
||||
// Verify that the participant appears in the visible remote participants
|
||||
const filmstripState = await getFilmstripState(p1);
|
||||
|
||||
await p1.log(`Dominant speaker: ${filmstripState.dominantSpeaker}`);
|
||||
await p1.log(`Visible remote participants: ${JSON.stringify(filmstripState.visibleRemoteParticipants)}`);
|
||||
await p1.log(`${name} endpoint ID: ${participantId}`);
|
||||
await p1.log(`Total remote: ${filmstripState.remoteParticipants.length}, Visible: ${filmstripState.visibleRemoteParticipants.length}`);
|
||||
|
||||
// Verify we actually have slot limitation (fewer visible than total)
|
||||
expect(filmstripState.visibleRemoteParticipants.length).toBeLessThan(filmstripState.remoteParticipants.length);
|
||||
|
||||
// Assert that the dominant speaker is in the visible participants
|
||||
// This is the key test - even though they may be alphabetically late and slots are limited,
|
||||
// they should still be visible because the fix reserves a slot for dominant speaker
|
||||
expect(filmstripState.visibleRemoteParticipants).toContain(participantId);
|
||||
|
||||
// Verify the dominant speaker thumbnail is visible in the filmstrip
|
||||
await p1.driver.$(`//span[@id='participant_${participantId}']`).waitForDisplayed({
|
||||
timeout: 5_000,
|
||||
timeoutMsg: `${name} dominant speaker thumbnail not visible in filmstrip`
|
||||
});
|
||||
|
||||
// Mute this participant back before testing the next one
|
||||
await participant.getToolbar().clickAudioMuteButton();
|
||||
await p1.driver.pause(2000);
|
||||
}
|
||||
|
||||
await hangupAllParticipants();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test dominant speaker in filmstrip with screensharing active.
|
||||
* Verifies that dominant speaker is still visible when screen shares
|
||||
* take up some of the visible slots.
|
||||
*/
|
||||
it.skip('testDominantSpeakerWithScreensharing', async () => {
|
||||
await ensureSixParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
const { p1, p2, p3, p4, p5, p6 } = ctx;
|
||||
|
||||
// Resize p1's window to limit filmstrip slots
|
||||
await p1.driver.setWindowSize(1024, 600);
|
||||
await p1.driver.pause(1000); // Wait for layout to adjust
|
||||
|
||||
// Set display names
|
||||
await setAlphabeticalDisplayNames(p1, p2, p3, p4, p5, p6);
|
||||
|
||||
// Start screensharing from p2
|
||||
await p2.getToolbar().clickDesktopSharingButton();
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
|
||||
// Test with multiple speakers while screensharing is active
|
||||
const speakersToTest = [
|
||||
{ participant: p4, name: 'Eve' },
|
||||
{ participant: p5, name: 'Frank' },
|
||||
{ participant: p6, name: 'Zoe' }
|
||||
];
|
||||
|
||||
for (const { participant, name } of speakersToTest) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await p1.log(`Testing ${name} (${participantId}) as dominant speaker with screensharing`);
|
||||
|
||||
// Make this participant the dominant speaker by unmuting
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Wait for the dominant speaker to be detected
|
||||
await waitForDominantSpeaker(p1, participantId, name);
|
||||
|
||||
// Verify dominant speaker is still visible in filmstrip despite screenshare
|
||||
const filmstripState = await getFilmstripState(p1);
|
||||
|
||||
await p1.log(`Dominant speaker (with screenshare): ${filmstripState.dominantSpeaker}`);
|
||||
await p1.log(`Visible remote participants with screenshare: ${JSON.stringify(filmstripState.visibleRemoteParticipants)}`);
|
||||
await p1.log(`${name} endpoint ID: ${participantId}`);
|
||||
await p1.log(`Total remote: ${filmstripState.remoteParticipants.length}, Visible: ${filmstripState.visibleRemoteParticipants.length}`);
|
||||
|
||||
// Verify we have slot limitation even with screensharing
|
||||
expect(filmstripState.visibleRemoteParticipants.length).toBeLessThan(filmstripState.remoteParticipants.length);
|
||||
|
||||
// The dominant speaker should still be in the visible participants despite screenshare taking slots
|
||||
expect(filmstripState.visibleRemoteParticipants).toContain(participantId);
|
||||
|
||||
// Verify thumbnail visibility
|
||||
await p1.driver.$(`//span[@id='participant_${participantId}']`).waitForDisplayed({
|
||||
timeout: 5_000,
|
||||
timeoutMsg: `${name} not visible with screensharing active`
|
||||
});
|
||||
|
||||
// Mute this participant back before testing the next one
|
||||
await participant.getToolbar().clickAudioMuteButton();
|
||||
await p1.driver.pause(1000);
|
||||
}
|
||||
|
||||
// Clean up - stop screensharing
|
||||
await p2.getToolbar().clickStopDesktopSharingButton();
|
||||
|
||||
await hangupAllParticipants();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that filmstrip maintains stable ordering when multiple speakers alternate.
|
||||
* Verifies that the alphabetical sorting prevents visual reordering when the same
|
||||
* set of speakers take turns speaking.
|
||||
*/
|
||||
it.skip('testFilmstripStableOrderingWithMultipleSpeakers', async () => {
|
||||
await ensureSixParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
const { p1, p2, p3, p4, p5, p6 } = ctx;
|
||||
|
||||
// Resize p1's window to limit filmstrip slots
|
||||
await p1.driver.setWindowSize(1024, 600);
|
||||
await p1.driver.pause(1000); // Wait for layout to adjust
|
||||
|
||||
// Set display names
|
||||
await setAlphabeticalDisplayNames(p1, p2, p3, p4, p5, p6);
|
||||
|
||||
// First, have Eve, Frank, and Zoe all speak to get them into the active speakers list
|
||||
const speakersToTest = [
|
||||
{ participant: p4, name: 'Eve' },
|
||||
{ participant: p5, name: 'Frank' },
|
||||
{ participant: p6, name: 'Zoe' }
|
||||
];
|
||||
|
||||
await p1.log('Initial round: getting all three speakers into active speakers list');
|
||||
|
||||
for (const { participant, name } of speakersToTest) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await p1.log(`${name} (${participantId}) speaking for the first time`);
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
await waitForDominantSpeaker(p1, participantId, name);
|
||||
await participant.getToolbar().clickAudioMuteButton();
|
||||
await p1.driver.pause(1000);
|
||||
}
|
||||
|
||||
// Now cycle through them again and verify they maintain alphabetical order (Eve, Frank, Zoe)
|
||||
await p1.log('Second round: verifying stable alphabetical ordering when speakers alternate');
|
||||
|
||||
const states = [];
|
||||
|
||||
for (const { participant, name } of speakersToTest) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await p1.log(`Testing ${name} (${participantId}) for stable ordering`);
|
||||
|
||||
// Make this participant the dominant speaker
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Wait for the dominant speaker to be detected
|
||||
await waitForDominantSpeaker(p1, participantId, name);
|
||||
|
||||
// Capture filmstrip state
|
||||
const filmstripState = await getFilmstripState(p1);
|
||||
|
||||
states.push({ name, id: participantId, state: filmstripState });
|
||||
|
||||
// Mute back
|
||||
await participant.getToolbar().clickAudioMuteButton();
|
||||
await p1.driver.pause(1000);
|
||||
}
|
||||
|
||||
const [ eveState, frankState, zoeState ] = states;
|
||||
|
||||
// Helper function to get participant names in the order they appear
|
||||
const getVisibleParticipantNames = async (visibleIds: string[]) => {
|
||||
return await p1.execute(ids => {
|
||||
const state = APP.store.getState();
|
||||
const participants = state['features/base/participants'];
|
||||
|
||||
return ids.map(id => {
|
||||
const participant = participants.remote.get(id);
|
||||
|
||||
return participant?.name || 'Unknown';
|
||||
});
|
||||
}, visibleIds);
|
||||
};
|
||||
|
||||
// Get the names of visible participants for each state
|
||||
const eveVisibleNames = await getVisibleParticipantNames(eveState.state.visibleRemoteParticipants);
|
||||
const frankVisibleNames = await getVisibleParticipantNames(frankState.state.visibleRemoteParticipants);
|
||||
const zoeVisibleNames = await getVisibleParticipantNames(zoeState.state.visibleRemoteParticipants);
|
||||
|
||||
await p1.log(`Visible participants when Eve is dominant: ${JSON.stringify(eveVisibleNames)}`);
|
||||
await p1.log(`Visible participants when Frank is dominant: ${JSON.stringify(frankVisibleNames)}`);
|
||||
await p1.log(`Visible participants when Zoe is dominant: ${JSON.stringify(zoeVisibleNames)}`);
|
||||
|
||||
await p1.log(`Eve visible count: ${eveState.state.visibleRemoteParticipants.length}, total remote: ${eveState.state.remoteParticipants.length}`);
|
||||
await p1.log(`Frank visible count: ${frankState.state.visibleRemoteParticipants.length}, total remote: ${frankState.state.remoteParticipants.length}`);
|
||||
await p1.log(`Zoe visible count: ${zoeState.state.visibleRemoteParticipants.length}, total remote: ${zoeState.state.remoteParticipants.length}`);
|
||||
|
||||
// Verify that each dominant speaker appears in visible participants
|
||||
expect(eveState.state.visibleRemoteParticipants).toContain(eveState.id);
|
||||
expect(frankState.state.visibleRemoteParticipants).toContain(frankState.id);
|
||||
expect(zoeState.state.visibleRemoteParticipants).toContain(zoeState.id);
|
||||
|
||||
// Helper function to get the relative order of Eve, Frank, and Zoe
|
||||
const getSpeakersOrder = (names: string[]) => {
|
||||
return names.filter(n => [ 'Eve', 'Frank', 'Zoe' ].includes(n));
|
||||
};
|
||||
|
||||
const eveOrder = getSpeakersOrder(eveVisibleNames);
|
||||
const frankOrder = getSpeakersOrder(frankVisibleNames);
|
||||
const zoeOrder = getSpeakersOrder(zoeVisibleNames);
|
||||
|
||||
await p1.log(`Speakers order when Eve is dominant: ${JSON.stringify(eveOrder)}`);
|
||||
await p1.log(`Speakers order when Frank is dominant: ${JSON.stringify(frankOrder)}`);
|
||||
await p1.log(`Speakers order when Zoe is dominant: ${JSON.stringify(zoeOrder)}`);
|
||||
|
||||
// Verify that the dominant speaker is always in the visible list (this tests the bug fix)
|
||||
expect(eveOrder).toContain('Eve');
|
||||
expect(frankOrder).toContain('Frank');
|
||||
expect(zoeOrder).toContain('Zoe');
|
||||
|
||||
// Helper to check if array is alphabetically sorted
|
||||
const isAlphabeticallySorted = (names: string[]) => {
|
||||
for (let i = 0; i < names.length - 1; i++) {
|
||||
if (names[i].localeCompare(names[i + 1]) > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Verify that whatever speakers ARE visible maintain alphabetical order
|
||||
// This is the key test - when the same speakers alternate, visible speakers stay in alphabetical order
|
||||
expect(isAlphabeticallySorted(eveOrder)).toBe(true);
|
||||
expect(isAlphabeticallySorted(frankOrder)).toBe(true);
|
||||
expect(isAlphabeticallySorted(zoeOrder)).toBe(true);
|
||||
|
||||
// Additionally verify order consistency: if multiple speakers are visible in multiple states,
|
||||
// their relative order should be the same
|
||||
// For example, if Eve and Frank are both visible when Zoe speaks, they should be [Eve, Frank]
|
||||
if (eveOrder.includes('Frank') && frankOrder.includes('Eve')) {
|
||||
// Both Eve and Frank visible in both states
|
||||
const eveAndFrankInEveState = eveOrder.filter(n => [ 'Eve', 'Frank' ].includes(n));
|
||||
const eveAndFrankInFrankState = frankOrder.filter(n => [ 'Eve', 'Frank' ].includes(n));
|
||||
|
||||
expect(eveAndFrankInEveState).toEqual(eveAndFrankInFrankState);
|
||||
}
|
||||
|
||||
await p1.log('Filmstrip maintains alphabetical ordering of visible speakers when dominant speaker changes');
|
||||
|
||||
await hangupAllParticipants();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -399,81 +95,3 @@ async function assertOneDominantSpeaker(participant: Participant) {
|
||||
expect(await participant.driver.$$(
|
||||
'//span[not(contains(@class, "tile-view"))]//span[contains(@class,"dominant-speaker")]').length).toBe(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a participant to be detected as the dominant speaker.
|
||||
*
|
||||
* @param {Participant} observer - The participant observing the dominant speaker state.
|
||||
* @param {string} participantId - The endpoint ID of the expected dominant speaker.
|
||||
* @param {string} participantName - The name of the participant for logging.
|
||||
*/
|
||||
async function waitForDominantSpeaker(
|
||||
observer: Participant,
|
||||
participantId: string,
|
||||
participantName: string
|
||||
): Promise<void> {
|
||||
await observer.driver.waitUntil(
|
||||
async () => {
|
||||
const state = await observer.execute(() => {
|
||||
const participants = APP.store.getState()['features/base/participants'];
|
||||
|
||||
return participants.dominantSpeaker;
|
||||
});
|
||||
|
||||
return state === participantId;
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: `${participantName} (${participantId}) was not detected as dominant speaker within 10 seconds`
|
||||
}
|
||||
);
|
||||
|
||||
// Wait a bit more for filmstrip state to update after dominant speaker changes
|
||||
await observer.driver.pause(1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current filmstrip state from Redux.
|
||||
*
|
||||
* @param {Participant} participant - The participant to query.
|
||||
* @returns {Promise<FilmstripState>} The filmstrip state.
|
||||
*/
|
||||
async function getFilmstripState(participant: Participant): Promise<{
|
||||
dominantSpeaker: string | null;
|
||||
remoteParticipants: string[];
|
||||
visibleRemoteParticipants: string[];
|
||||
}> {
|
||||
return await participant.execute(() => {
|
||||
const state = APP.store.getState();
|
||||
const filmstrip = state['features/filmstrip'];
|
||||
const participants = state['features/base/participants'];
|
||||
|
||||
return {
|
||||
dominantSpeaker: participants.dominantSpeaker,
|
||||
remoteParticipants: filmstrip.remoteParticipants,
|
||||
visibleRemoteParticipants: Array.from(filmstrip.visibleRemoteParticipants)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set display names for all 6 participants to create alphabetical ordering.
|
||||
*/
|
||||
async function setAlphabeticalDisplayNames(
|
||||
p1: Participant,
|
||||
p2: Participant,
|
||||
p3: Participant,
|
||||
p4: Participant,
|
||||
p5: Participant,
|
||||
p6: Participant
|
||||
): Promise<void> {
|
||||
await p1.setLocalDisplayName('Alice');
|
||||
await p2.setLocalDisplayName('Bob');
|
||||
await p3.setLocalDisplayName('Charlie');
|
||||
await p4.setLocalDisplayName('Eve');
|
||||
await p5.setLocalDisplayName('Frank');
|
||||
await p6.setLocalDisplayName('Zoe');
|
||||
|
||||
// Wait for display names to propagate
|
||||
await p1.driver.pause(2000);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ const specs = [
|
||||
*/
|
||||
function generateCapabilitiesFromSpecs(): Record<string, any> {
|
||||
const allSpecFiles: string[] = [];
|
||||
const browsers = [ 'p1', 'p2', 'p3', 'p4', 'p5', 'p6' ];
|
||||
const browsers = [ 'p1', 'p2', 'p3', 'p4' ];
|
||||
|
||||
for (const pattern of specs) {
|
||||
const matches = glob.sync(pattern, { cwd: path.join(__dirname) });
|
||||
@@ -87,9 +87,7 @@ function generateCapabilitiesFromSpecs(): Record<string, any> {
|
||||
p1: new Set(),
|
||||
p2: new Set(),
|
||||
p3: new Set(),
|
||||
p4: new Set(),
|
||||
p5: new Set(),
|
||||
p6: new Set()
|
||||
p4: new Set()
|
||||
};
|
||||
|
||||
for (const file of allSpecFiles) {
|
||||
|
||||
Reference in New Issue
Block a user