Compare commits

...

4 Commits

Author SHA1 Message Date
Calin-Teodor
417c38ab9e fix(filmstrip): keep AudioTracksContainer in the DOM while Filmstrip is hidden in reduced UI 2026-01-27 17:53:57 +02:00
Calinteodor
33a4245a1f fix(lobby): reset lobbyVisible state when we a potential conference is being left (#16881)
* Reset lobby state when we we dispatch conferenceWillLeave action.
2026-01-27 12:24:16 +02:00
Mihaela Dumitru
2eb07cb79f feat(participants): store user context data for external API events and functions 2026-01-27 10:42:02 +01:00
damencho
63e4c41d92 fix(tests): Fixes tests stack traces.
Before:
```
Error: element ("[data-testid="participant1-more-options-ba16a58a"]") still not existing after 1000ms
    at async ParticipantsPane.openParticipantContextMenu (/tmp/tmp.bMVHEDmYin/jitsi-meet/tests/pageobjects/ParticipantsPane.ts:202:9)
    at async BreakoutRooms.sendParticipantToBreakoutRoom (/tmp/tmp.bMVHEDmYin/jitsi-meet/tests/pageobjects/BreakoutRooms.ts:205:9)
```

After:
```
Error: element ("[data-testid="participant1-more-options-ecea6dd6"]") still not existing after 1000ms
    at async ParticipantsPane.openParticipantContextMenu (/tmp/tmp.j8VkoO9abR/jitsi-meet/tests/pageobjects/ParticipantsPane.ts:202:9)
    at async BreakoutRooms.sendParticipantToBreakoutRoom (/tmp/tmp.j8VkoO9abR/jitsi-meet/tests/pageobjects/BreakoutRooms.ts:205:9)
    at async Context.<anonymous> (/tmp/tmp.j8VkoO9abR/jitsi-meet/tests/specs/misc/breakoutRooms.spec.ts:349:9)
```
2026-01-26 12:30:32 -06:00
15 changed files with 159 additions and 34 deletions

View File

@@ -1,4 +1,4 @@
import { setRoom } from '../base/conference/actions';
import { setRoom } from '../base/conference/actions.native';
import { getConferenceState } from '../base/conference/functions';
import {
configWillLoad,
@@ -29,7 +29,7 @@ import {
} from '../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../mobile/navigation/routes';
import { clearNotifications } from '../notifications/actions';
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions';
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions.native';
import { maybeRedirectToTokenAuthUrl } from './actions.any';
import { addTrackStateToURL, getDefaultURL } from './functions.native';

View File

@@ -93,10 +93,17 @@ export function commonUserJoinedHandling(
if (!user.isHidden()) {
const isReplacing = user?.isReplacing();
const isPromoted = conference?.getMetadataHandler().getMetadata()?.visitors?.promoted?.[id];
const userIdentity = user.getIdentity()?.user;
// Map identity from JWT context to userContext for external API
const userContext = userIdentity ? {
id: userIdentity.id,
name: userIdentity.name
} : undefined;
// the identity and avatar come from jwt and never change in the presence
dispatch(participantJoined({
avatarURL: user.getIdentity()?.user?.avatar,
avatarURL: userIdentity?.avatar,
botType: user.getBotType(),
conference,
id,
@@ -105,7 +112,8 @@ export function commonUserJoinedHandling(
role: user.getRole(),
isPromoted,
isReplacing,
sources: user.getSources()
sources: user.getSources(),
userContext
}));
}
}

View File

@@ -4,6 +4,7 @@ import { getCustomerDetails } from '../../jaas/actions.any';
import { getJaasJWT, isVpaasMeeting } from '../../jaas/functions';
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../../mobile/navigation/routes';
import { conferenceWillLeave } from '../conference/actions.native';
import { setJWT } from '../jwt/actions';
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
@@ -58,5 +59,8 @@ export function connect(id?: string, password?: string) {
* @returns {Function}
*/
export function hangup(_requestFeedback = false) {
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
return (dispatch: IStore['dispatch']) => {
dispatch(conferenceWillLeave());
dispatch(appNavigate(undefined));
};
}

View File

@@ -606,13 +606,21 @@ function _e2eeUpdated({ getState, dispatch }: IStore, conference: IJitsiConferen
function _localParticipantJoined({ getState, dispatch }: IStore, next: Function, action: AnyAction) {
const result = next(action);
const settings = getState()['features/base/settings'];
const state = getState();
const settings = state['features/base/settings'];
const jwtUser = state['features/base/jwt']?.user;
const userContext = jwtUser ? {
id: jwtUser.id,
name: jwtUser.name
} : undefined;
dispatch(localParticipantJoined({
avatarURL: settings.avatarURL,
email: settings.email,
name: settings.displayName,
id: ''
id: '',
userContext
}));
return result;

View File

@@ -627,7 +627,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) {
pinned,
presence,
role,
sources
sources,
userContext
} = participant;
let { conference, id } = participant;
@@ -659,7 +660,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) {
pinned: pinned || false,
presence,
role: role || PARTICIPANT_ROLE.NONE,
sources
sources,
userContext
};
}

View File

@@ -41,6 +41,12 @@ export interface IParticipant {
role?: string;
sources?: Map<string, Map<string, ISourceInfo>>;
supportsRemoteControl?: boolean;
userContext?: IUserContext;
}
export interface IUserContext {
id?: string;
name?: string;
}
export interface ILocalParticipant extends IParticipant {

View File

@@ -44,6 +44,16 @@ export const getMainRoom = (stateful: IStateful) => {
* @returns {IRoomsInfo} The rooms info.
*/
export const getRoomsInfo = (stateful: IStateful) => {
const state = toState(stateful);
const localParticipant = getLocalParticipant(stateful);
const jwtUser = state['features/base/jwt']?.user;
const localUserContext = jwtUser ? {
id: jwtUser.id,
name: jwtUser.name
} : {
id: localParticipant?.jwtId,
name: localParticipant?.name
};
const breakoutRooms = getBreakoutRooms(stateful);
const conference = getCurrentConference(stateful);
@@ -57,7 +67,6 @@ export const getRoomsInfo = (stateful: IStateful) => {
const conferenceParticipants = conference?.getParticipants()
.filter((participant: IJitsiParticipant) => !participant.isHidden());
const localParticipant = getLocalParticipant(stateful);
let localParticipantInfo;
if (localParticipant) {
@@ -65,7 +74,8 @@ export const getRoomsInfo = (stateful: IStateful) => {
role: localParticipant.role,
displayName: localParticipant.name,
avatarUrl: localParticipant.loadableAvatarUrl,
id: localParticipant.id
id: localParticipant.id,
userContext: localUserContext
};
}
@@ -86,7 +96,8 @@ export const getRoomsInfo = (stateful: IStateful) => {
role: participantItem.getRole(),
displayName: participantItem.getDisplayName(),
avatarUrl: storeParticipant?.loadableAvatarUrl,
id: participantItem.getId()
id: participantItem.getId(),
userContext: storeParticipant?.userContext
} as IRoomInfoParticipant;
}) ]
: [ localParticipantInfo ]
@@ -110,13 +121,18 @@ export const getRoomsInfo = (stateful: IStateful) => {
const storeParticipant = getParticipantById(stateful,
ids.length > 1 ? ids[1] : participantItem.jid);
// Check if this is the local participant
const isLocal = storeParticipant?.id === localParticipant?.id;
const userContext = isLocal ? localUserContext : (storeParticipant?.userContext || participantItem.userContext);
return {
jid: participantItem?.jid,
role: participantItem?.role,
displayName: participantItem?.displayName,
avatarUrl: storeParticipant?.loadableAvatarUrl,
id: storeParticipant ? storeParticipant.id
: participantLongId
: participantLongId,
userContext
} as IRoomInfoParticipant;
}) : []
} as IRoomInfo;

View File

@@ -44,19 +44,59 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
switch (type) {
case UPDATE_BREAKOUT_ROOMS: {
// edit name if it was overwritten
// Enrich participants with userContext from Redux store
if (!action.updatedNames) {
const { overwrittenNameList } = getState()['features/base/participants'];
const state = getState();
const { overwrittenNameList, local: localParticipant } = state['features/base/participants'];
const jwtUser = state['features/base/jwt']?.user;
const localUserContext = jwtUser ? {
id: jwtUser.id,
name: jwtUser.name
} : {
id: localParticipant?.jwtId,
name: localParticipant?.name
};
if (Object.keys(overwrittenNameList).length > 0) {
const newRooms: IRooms = {};
// Get existing userContext cache
const existingCache = state['features/breakout-rooms'].userContextCache || {};
const newCache = { ...existingCache };
Object.entries(action.rooms as IRooms).forEach(([ key, r ]) => {
let participants = r?.participants || {};
let jid;
const newRooms: IRooms = {};
Object.entries(action.rooms as IRooms).forEach(([ key, r ]) => {
let participants = r?.participants || {};
// Add userContext to each participant
const enhancedParticipants: typeof participants = {};
for (const [ participantJid, participantData ] of Object.entries(participants)) {
const ids = participantJid.split('/');
const participantId = ids.length > 1 ? ids[1] : participantData.jid;
const storeParticipant = getParticipantById(state, participantId);
const isLocal = storeParticipant?.id === localParticipant?.id;
// Try to get userContext from: local, store, cache, or incoming data
const userContext = isLocal
? localUserContext
: (storeParticipant?.userContext || newCache[participantId] || participantData.userContext);
// Update cache if we have userContext
if (userContext && participantId) {
newCache[participantId] = userContext;
}
enhancedParticipants[participantJid] = {
...participantData,
userContext
};
}
participants = enhancedParticipants;
// Apply overwritten display names
if (Object.keys(overwrittenNameList).length > 0) {
for (const id of Object.keys(overwrittenNameList)) {
jid = Object.keys(participants).find(p => p.slice(p.indexOf('/') + 1) === id);
const jid = Object.keys(participants).find(p => p.slice(p.indexOf('/') + 1) === id);
if (jid) {
participants = {
@@ -68,15 +108,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
};
}
}
}
newRooms[key] = {
...r,
participants
};
});
newRooms[key] = {
...r,
participants
};
});
action.rooms = newRooms;
}
action.rooms = newRooms;
action.userContextCache = newCache;
}
// edit the chat history to match names for participants in breakout rooms

View File

@@ -10,12 +10,19 @@ import { IRooms } from './types';
const DEFAULT_STATE = {
rooms: {},
roomCounter: 0
roomCounter: 0,
userContextCache: {}
};
export interface IBreakoutRoomsState {
roomCounter: number;
rooms: IRooms;
userContextCache: {
[participantId: string]: {
id?: string;
name?: string;
};
};
}
/**
@@ -29,12 +36,13 @@ ReducerRegistry.register<IBreakoutRoomsState>(FEATURE_KEY, (state = DEFAULT_STAT
roomCounter: action.roomCounter
};
case UPDATE_BREAKOUT_ROOMS: {
const { roomCounter, rooms } = action;
const { roomCounter, rooms, userContextCache } = action;
return {
...state,
roomCounter,
rooms
rooms,
userContextCache: userContextCache || state.userContextCache
};
}
case _RESET_BREAKOUT_ROOMS: {

View File

@@ -8,6 +8,10 @@ export interface IRoom {
displayName: string;
jid: string;
role: string;
userContext?: {
id?: string;
name?: string;
};
};
};
}
@@ -33,4 +37,8 @@ export interface IRoomInfoParticipant {
id: string;
jid: string;
role: string;
userContext?: {
id?: string;
name?: string;
};
}

View File

@@ -257,6 +257,9 @@ class Conference extends AbstractConference<IProps, any> {
id = 'videospace'
onTouchStart = { this._onVideospaceTouchStart }>
<LargeVideo />
<StageFilmstrip />
<ScreenshareFilmstrip />
<MainFilmstrip />
</div>
<span
aria-level = { 1 }

View File

@@ -212,7 +212,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const { defaultRemoteDisplayName } = state['features/base/config'];
const { participant } = action;
const { fakeParticipant, id, local, name } = participant;
const { fakeParticipant, id, local, name, userContext } = participant;
// The version of external api outside of middleware did not emit
// the local participant being created.
@@ -225,7 +225,8 @@ MiddlewareRegistry.register(store => next => action => {
APP.API.notifyUserJoined(id, {
displayName: name,
formattedDisplayName: appendSuffix(
name || defaultRemoteDisplayName)
name || defaultRemoteDisplayName),
userContext
});
}

View File

@@ -338,6 +338,11 @@ export interface IProps extends WithTranslation {
*/
_maxTopPanelHeight: number;
/**
* Whethere reduced UI feature is enabled or not.
*/
_reducedUI: boolean;
/**
* The participants in the call.
*/
@@ -546,6 +551,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
_filmstripDisabled,
_localScreenShareId,
_mainFilmstripVisible,
_reducedUI,
_resizableFilmstrip,
_topPanelFilmstrip,
_topPanelMaxHeight,
@@ -589,6 +595,13 @@ class Filmstrip extends PureComponent <IProps, IState> {
}
}
// FIX: Until we move AudioTracksContainer to a more global place,
// we apply this css hot fix to hide Filmstrip but keep AudioTracksContainer in the DOM,
// so we don't have audio problems when reduced UI is enabled.
if (_reducedUI) {
filmstripStyle.display = 'none';
}
let toolbar: React.ReactNode = null;
if (!this.props._iAmRecorder && this.props._isFilmstripButtonEnabled
@@ -1120,6 +1133,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const _currentLayout = getCurrentLayout(state);
const _isVerticalFilmstrip = _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|| (filmstripType === FILMSTRIP_TYPE.MAIN && _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
const { reducedUI } = state['features/base/responsive-ui'];
return {
_className: className,
@@ -1137,6 +1151,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
_mainFilmstripVisible: notDisabled,
_maxFilmstripWidth: videoSpaceWidth - MIN_STAGE_VIEW_WIDTH,
_maxTopPanelHeight: clientHeight - MIN_STAGE_VIEW_HEIGHT,
_reducedUI: reducedUI,
_remoteParticipantsLength: _remoteParticipants?.length ?? 0,
_topPanelHeight: topPanelHeight.current,
_topPanelMaxHeight: topPanelHeight.current || TOP_FILMSTRIP_HEIGHT,

View File

@@ -2,6 +2,7 @@ import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_LEAVE,
SET_PASSWORD
} from '../base/conference/actionTypes';
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
@@ -69,11 +70,13 @@ ReducerRegistry.register<ILobbyState>('features/lobby', (state = DEFAULT_STATE,
}
case CONFERENCE_JOINED:
case CONFERENCE_LEFT:
case CONFERENCE_WILL_LEAVE:
return {
...state,
isDisplayNameRequiredError: false,
knocking: false,
passwordJoinFailed: false
lobbyVisible: false,
passwordJoinFailed: false,
};
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED:
return _knockingParticipantArrivedOrUpdated(action.participant, state);

View File

@@ -194,6 +194,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
} ]
],
execArgv: [ '--stack-trace-limit=100' ],
// =====
// Hooks
// =====