Compare commits

...

20 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
damencho
2c6ccd7d6b fix(transcriptions): Fixes stop transcriptions via api. 2026-01-23 13:39:48 -06:00
damencho
4ce27eeb1a chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2118.0.0+67fd2c84...v2124.0.0+80df84a1
2026-01-23 12:34:38 -06:00
damencho
11453dcc79 fix(transcriptions): Fixes stop transcriptions in some cases. 2026-01-23 10:18:23 -06:00
damencho
3375ee49bd fix(tests): Ignore clickable for password dialog.
It gives strange error that is not clickable, but it is seen on the screenshot and test passes without it.
2026-01-23 06:03:06 -06:00
damencho
e45df58cfb fix(tests): Fixes a problem with two notifications. 2026-01-23 06:03:06 -06:00
Calin-Teodor
7d628960d7 feat(breakout-rooms): button margin style fix 2026-01-22 10:52:48 -06:00
Calin-Teodor
cf13b8f0ba fix(responsive-ui): apply reducedUI only when both width and height are below threshold 2026-01-22 10:52:48 -06:00
Jaya Allamsetty
e106109090 fix(recording) Restores track mute state to original state after consent is provided.
Do not forcefully unmute audio and video if the user consents to being recorded and wants to stay unmuted but was muted before hitting join.
2026-01-22 10:11:43 -05:00
Calin-Teodor
fc170891cb feat(subtitles): rework how isAsyncTranscriptionEnabled is being used 2026-01-22 14:00:06 +02:00
Calin-Teodor
775cc52f66 feat(subtitles): hide translation UI for both web and native 2026-01-22 10:50:03 +02:00
Calin-Teodor
3baede6ff1 feat(config): add reducedUIEnabled config to control web side 2026-01-22 10:44:15 +02:00
Дамян Минков
9462a9ce36 feat(breakout-rooms): Adds some jid validation on joining. (#16858)
* feat(breakout-rooms): Adds some jid validation on joining.

* squash: Fix comments.

* Update resources/prosody-plugins/mod_muc_breakout_rooms.lua

Co-authored-by: bgrozev <boris@jitsi.org>

* Update resources/prosody-plugins/mod_muc_breakout_rooms.lua

Co-authored-by: bgrozev <boris@jitsi.org>

---------

Co-authored-by: bgrozev <boris@jitsi.org>
2026-01-21 19:53:27 -06:00
Aaron van Meerten
4713062200 feat(muc_auth_ban): add metrics and logs for error responses from access backend (#16853) 2026-01-21 13:47:32 +01:00
Calinteodor
65eb2a2899 feat(base/modal): small updates around JitsiScreen footer (#16727)
*UI updates and reworks around JitsiScreen footer and its children.
2026-01-21 13:05:05 +02:00
Boris Grozev
f0452d05b9 feat: Hide the translation UI when asyncTranscription is used. 2026-01-20 11:56:13 -06:00
damencho
1102f4205a fix: Fixes setting backend recording when async transcriptions are not on. 2026-01-20 11:56:06 -06:00
60 changed files with 547 additions and 292 deletions

View File

@@ -927,6 +927,9 @@ var config = {
// [ 'microphone', 'camera' ]
// ],
// Enable reduced UI on web.
// reducedUIEnabled: true,
// Overrides the buttons displayed in the main toolbar for reduced UI.
// When there isn't an override for a certain configuration the default jitsi-meet configuration will be used.
// The order of the buttons in the array is preserved.

View File

@@ -1407,7 +1407,7 @@ PODS:
- Yoga
- react-native-performance (5.1.2):
- React-Core
- react-native-safe-area-context (5.5.2):
- react-native-safe-area-context (5.6.1):
- React-Core
- react-native-slider (4.5.6):
- DoubleConversion
@@ -2271,7 +2271,7 @@ SPEC CHECKSUMS:
react-native-orientation-locker: dbd3f6ddbe9e62389cb0807dc2af63f6c36dec36
react-native-pager-view: 11662c698c8f11d39e05891316d2a144fa00adc4
react-native-performance: 125a96c145e29918b55b45ce25cbba54f1e24dcd
react-native-safe-area-context: 0f7bf11598f9a61b7ceac8dc3f59ef98697e99e1
react-native-safe-area-context: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
react-native-slider: 1205801a8d29b28cacc14eef08cb120015cdafcb
react-native-video: eb861d67a71dfef1bbf6086a811af5f338b13781
react-native-webrtc: e8f0ce746353adc2744a2b933645e1aeb41eaa74

View File

@@ -790,7 +790,7 @@ function initCommands() {
}
if (transcription) {
APP.store.dispatch(setRequestingSubtitles(true, false, null));
APP.store.dispatch(setRequestingSubtitles(true, false, null, true));
}
},
@@ -812,7 +812,7 @@ function initCommands() {
}
if (transcription) {
APP.store.dispatch(setRequestingSubtitles(false, false, null));
APP.store.dispatch(setRequestingSubtitles(false, false, null, true));
}
if (mode === 'local') {

24
package-lock.json generated
View File

@@ -66,7 +66,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -91,7 +91,7 @@
"react-native-pager-view": "6.8.1",
"react-native-paper": "5.10.3",
"react-native-performance": "5.1.2",
"react-native-safe-area-context": "5.5.2",
"react-native-safe-area-context": "5.6.1",
"react-native-screens": "4.11.1",
"react-native-sound": "https://github.com/jitsi/react-native-sound.git#ea13c97b5c2a4ff5e0d9bacbd9ff5e4457fe2c3c",
"react-native-splash-view": "0.0.18",
@@ -18360,8 +18360,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
"integrity": "sha512-V0fp3/ZJRjHZgRlpzfCcdIF4nZ+WWWkAxKuWVTMHXfcZj07aQUHTiwIukgRludtaj9RcyNcqCVC0PrGBeeUzTw==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "^2.6.7",
@@ -22002,9 +22002,9 @@
}
},
"node_modules/react-native-safe-area-context": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz",
"integrity": "sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw==",
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz",
"integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==",
"license": "MIT",
"peerDependencies": {
"react": "*",
@@ -39717,8 +39717,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
"integrity": "sha512-V0fp3/ZJRjHZgRlpzfCcdIF4nZ+WWWkAxKuWVTMHXfcZj07aQUHTiwIukgRludtaj9RcyNcqCVC0PrGBeeUzTw==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
"requires": {
"@jitsi/js-utils": "^2.6.7",
"@jitsi/logger": "2.1.1",
@@ -42378,9 +42378,9 @@
"integrity": "sha512-l5JOJphNzox9a9icL3T6O/gEqZuqWqcbejW04WPa10m0UanBdIYrNkPFl48B3ivWw3MabpjB6GiDYv7old9/fw=="
},
"react-native-safe-area-context": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz",
"integrity": "sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw=="
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz",
"integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA=="
},
"react-native-screens": {
"version": "4.11.1",

View File

@@ -72,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -97,7 +97,7 @@
"react-native-pager-view": "6.8.1",
"react-native-paper": "5.10.3",
"react-native-performance": "5.1.2",
"react-native-safe-area-context": "5.5.2",
"react-native-safe-area-context": "5.6.1",
"react-native-screens": "4.11.1",
"react-native-sound": "https://github.com/jitsi/react-native-sound.git#ea13c97b5c2a4ff5e0d9bacbd9ff5e4457fe2c3c",
"react-native-splash-view": "0.0.18",

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

@@ -81,7 +81,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
break;
}
case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
let descriptionKey;
let titleKey;
let uid = '';
const localParticipant = getLocalParticipant(getState);
@@ -111,8 +110,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
!raisedHand && dispatch(raiseHand(true));
dispatch(hideNotification(uid));
}) ],
descriptionKey,
sticky: true,
titleKey,
uid
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
@@ -271,7 +268,6 @@ StateListenerRegistry.register(
dispatch(showNotification({
titleKey: 'notify.hostAskedUnmute',
sticky: true,
customActionNameKey,
customActionHandler,
uid: ASKED_TO_UNMUTE_NOTIFICATION_ID

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

@@ -559,6 +559,7 @@ export interface IConfig {
skipConsentInMeeting?: boolean;
suggestRecording?: boolean;
};
reducedUIEnabled?: boolean;
reducedUImainToolbarButtons?: Array<string>;
remoteVideoMenu?: {
disableDemote?: boolean;

View File

@@ -215,6 +215,7 @@ export default [
'recordings.showPrejoinWarning',
'recordings.showRecordingLink',
'recordings.suggestRecording',
'reducedUIEnabled',
'reducedUImainToolbarButtons',
'replaceParticipant',
'resolution',

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

@@ -63,7 +63,7 @@ const JitsiScreen = ({
footerComponent,
hasBottomTextInput = false,
hasExtraHeaderHeight = false,
safeAreaInsets = [ 'left', 'right' ],
safeAreaInsets = [ 'bottom', 'left', 'right' ],
style
}: IProps) => {
const renderContent = () => (
@@ -78,8 +78,8 @@ const JitsiScreen = ({
edges = { safeAreaInsets }
style = { styles.safeArea }>
{ children }
{ footerComponent?.() }
</SafeAreaView>
{ footerComponent?.() }
</JitsiKeyboardAvoidingView>
);

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

@@ -43,6 +43,7 @@ export function clientResized(clientWidth: number, clientHeight: number) {
if (navigator.product !== 'ReactNative') {
const state = getState();
const { reducedUIEnabled = true } = state['features/base/config'];
const { isOpen: isChatOpen, width } = state['features/chat'];
if (isChatOpen) {
@@ -51,7 +52,7 @@ export function clientResized(clientWidth: number, clientHeight: number) {
availableWidth -= getParticipantsPaneWidth(state);
dispatch(setReducedUI(availableWidth, clientHeight));
reducedUIEnabled && dispatch(setReducedUI(availableWidth, clientHeight));
}
batch(() => {
@@ -112,7 +113,7 @@ export function setReducedUI(width: number, height: number) {
const threshold = navigator.product === 'ReactNative'
? REDUCED_UI_THRESHOLD
: WEB_REDUCED_UI_THRESHOLD;
const reducedUI = Math.min(width, height) < threshold;
const reducedUI = Math.max(width, height) < threshold;
if (reducedUI !== getState()['features/base/responsive-ui'].reducedUI) {
return dispatch({

View File

@@ -8,7 +8,7 @@ export default {
button: {
marginBottom: BaseTheme.spacing[4],
marginHorizontal: BaseTheme.spacing[2]
marginHorizontal: BaseTheme.spacing[3]
},
collapsibleList: {

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

@@ -1,39 +1,29 @@
/* eslint-disable react/no-multi-comp */
import { Route, useIsFocused } from '@react-navigation/native';
import React, { Component, useEffect } from 'react';
import { connect } from 'react-redux';
import { connect, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { StyleType } from '../../../base/styles/functions.native';
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
import { getUnreadPollCount } from '../../../polls/functions';
import { pollsStyles } from '../../../polls/components/native/styles';
import { closeChat, sendMessage } from '../../actions.native';
import { getUnreadFilesCount } from '../../functions';
import { ChatTabs } from '../../constants';
import { IChatProps as AbstractProps } from '../../types';
import ChatInputBar from './ChatInputBar';
import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
import styles from './styles';
interface IProps extends AbstractProps {
/**
* The number of unread file messages.
*/
_unreadFilesCount: number;
/**
* The number of unread messages.
*/
_unreadMessagesCount: number;
/**
* The number of unread polls.
*/
_unreadPollsCount: number;
/**
* Default prop for navigating between screen components(React Navigation).
*/
@@ -62,6 +52,7 @@ class Chat extends Component<IProps> {
// Bind event handlers so they are only bound once per instance.
this._onSendMessage = this._onSendMessage.bind(this);
this._renderFooter = this._renderFooter.bind(this);
}
/**
@@ -76,14 +67,10 @@ class Chat extends Component<IProps> {
return (
<JitsiScreen
disableForcedKeyboardDismiss = { true }
/* eslint-disable react/jsx-no-bind */
footerComponent = { () =>
<ChatInputBar onSend = { this._onSendMessage } />
}
footerComponent = { this._renderFooter }
hasBottomTextInput = { true }
hasExtraHeaderHeight = { true }
style = { styles.chatContainer }>
style = { pollsStyles.pollPaneContainer as StyleType }>
{/* @ts-ignore */}
<MessageContainer messages = { _messages } />
<MessageRecipient privateMessageRecipient = { privateMessageRecipient } />
@@ -91,6 +78,16 @@ class Chat extends Component<IProps> {
);
}
/**
* Renders the footer component.
*
* @private
* @returns {React$Element<*>}
*/
_renderFooter() {
return <ChatInputBar onSend = { this._onSendMessage } />;
}
/**
* Sends a text message.
*
@@ -113,9 +110,7 @@ class Chat extends Component<IProps> {
* @private
* @returns {{
* _messages: Array<Object>,
* _unreadMessagesCount: number,
* _unreadPollsCount: number,
* _unreadFilesCount: number
* _unreadMessagesCount: number
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
@@ -123,34 +118,34 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
return {
_messages: messages,
_unreadMessagesCount: unreadMessagesCount,
_unreadPollsCount: getUnreadPollCount(state),
_unreadFilesCount: getUnreadFilesCount(state)
_unreadMessagesCount: unreadMessagesCount
};
}
export default translate(connect(_mapStateToProps)((props: IProps) => {
const { _unreadMessagesCount, _unreadPollsCount, _unreadFilesCount, dispatch, navigation, t } = props;
const totalUnread = _unreadMessagesCount + _unreadPollsCount + _unreadFilesCount;
const unreadMessagesNr = totalUnread > 0;
const { _unreadMessagesCount, dispatch, navigation, t } = props;
const isChatTabFocused = useSelector((state: IReduxState) => state['features/chat'].focusedTab === ChatTabs.CHAT);
const isFocused = useIsFocused();
const activeUnreadMessagesNr = !isChatTabFocused && _unreadMessagesCount > 0;
useEffect(() => {
navigation?.setOptions({
tabBarLabel: () => (
<TabBarLabelCounter
activeUnreadNr = { unreadMessagesNr }
activeUnreadNr = { activeUnreadMessagesNr }
isFocused = { isFocused }
label = { t('chat.tabs.chat') }
unreadCount = { totalUnread } />
unreadCount = { _unreadMessagesCount } />
)
});
return () => {
isFocused && dispatch(closeChat());
};
}, [ isFocused, _unreadMessagesCount, _unreadPollsCount, _unreadFilesCount ]);
}, [ isFocused, _unreadMessagesCount ]);
return (
<Chat { ...props } />

View File

@@ -7,7 +7,6 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconSend } from '../../../base/icons/svg';
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
import IconButton from '../../../base/ui/components/native/IconButton';
import Input from '../../../base/ui/components/native/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
@@ -85,14 +84,6 @@ class ChatInputBar extends Component<IProps, IState> {
* @inheritdoc
*/
override render() {
let inputBarStyles;
if (this.props.aspectRatio === ASPECT_RATIO_WIDE) {
inputBarStyles = styles.inputBarWide;
} else {
inputBarStyles = styles.inputBarNarrow;
}
if (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) {
return (
<View
@@ -109,7 +100,7 @@ class ChatInputBar extends Component<IProps, IState> {
<View
id = 'chat-input'
style = { [
inputBarStyles,
styles.inputBar,
this.state.addPadding ? styles.extraBarPadding : null
] as ViewStyle[] }>
<Input
@@ -129,6 +120,7 @@ class ChatInputBar extends Component<IProps, IState> {
id = { this.props.t('chat.sendButton') }
onPress = { this._onSubmit }
src = { IconSend }
style = { styles.sendButton }
type = { BUTTON_TYPES.PRIMARY } />
</View>
);

View File

@@ -42,6 +42,8 @@ const ClosedCaptions = ({
const navigateToLanguageSelect = useCallback(() => {
navigate(screen.conference.subtitles);
}, [ navigation, screen ]);
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
useEffect(() => {
navigation?.setOptions({
@@ -91,17 +93,20 @@ const ClosedCaptions = ({
return (
<>
<View style = { closedCaptionsStyles.languageButtonContainer as ViewStyle }>
<Text style = { closedCaptionsStyles.languageButtonText }>{ t('transcribing.translateTo') }:</Text>
<TouchableHighlight onPress = { navigateToLanguageSelect }>
<View style = { closedCaptionsStyles.languageButtonContent as ViewStyle }>
<Text style = { closedCaptionsStyles.languageButtonText }>{ t(selectedLanguage ?? 'transcribing.subtitlesOff') }</Text>
<Icon
size = { 24 }
src = { IconArrowRight } />
</View>
</TouchableHighlight>
</View>
{
// Hide the "Translate to" option when asyncTranscription is enabled
!isAsyncTranscriptionEnabled && <View style = { closedCaptionsStyles.languageButtonContainer as ViewStyle }>
<Text style = { closedCaptionsStyles.languageButtonText }>{ t('transcribing.translateTo') }:</Text>
<TouchableHighlight onPress = { navigateToLanguageSelect }>
<View style = { closedCaptionsStyles.languageButtonContent as ViewStyle }>
<Text style = { closedCaptionsStyles.languageButtonText }>{ t(selectedLanguage ?? 'transcribing.subtitlesOff') }</Text>
<Icon
size = { 24 }
src = { IconArrowRight } />
</View>
</TouchableHighlight>
</View>
}
<View style = { closedCaptionsStyles.messagesContainer as ViewStyle }>
<SubtitlesMessagesContainer
groups = { groupedSubtitles }

View File

@@ -14,12 +14,6 @@ const recipientContainer = {
padding: BaseTheme.spacing[2]
};
const inputBar = {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between'
};
/**
* The styles of the feature chat.
*
@@ -143,23 +137,23 @@ export default {
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
*/
extraBarPadding: {
paddingBottom: 30
paddingBottom: BaseTheme.spacing[8]
},
inputBarNarrow: {
...inputBar,
height: 112,
marginHorizontal: BaseTheme.spacing[3]
inputBar: {
alignSelf: 'stretch',
flexDirection: 'row',
width: '100%'
},
inputBarWide: {
...inputBar,
height: 88,
marginHorizontal: BaseTheme.spacing[9]
sendButton: {
marginRight: BaseTheme.spacing[5],
marginLeft: BaseTheme.spacing[2]
},
customInputContainer: {
width: '75%'
marginLeft: BaseTheme.spacing[5],
flex: 1
},
messageBubble: {
@@ -215,11 +209,6 @@ export default {
fontSize: 13
},
chatContainer: {
backgroundColor: BaseTheme.palette.ui01,
flex: 1
},
tabContainer: {
flexDirection: 'row',
justifyContent: 'center'

View File

@@ -21,10 +21,9 @@ const titleBarSafeView = {
export default {
bottomContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
bottom: BaseTheme.spacing[8]
display: 'flex',
flexDirection: 'column'
},
/**
@@ -101,7 +100,7 @@ export default {
},
soundDeviceButton: {
marginBottom: BaseTheme.spacing[3],
marginVertical: BaseTheme.spacing[3],
width: 240
},
@@ -162,7 +161,6 @@ export default {
videoStoppedLabel: {
...BaseTheme.typography.bodyShortRegularLarge,
color: BaseTheme.palette.text01,
marginBottom: BaseTheme.spacing[3],
textAlign: 'center',
width: '100%'
},

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

@@ -24,8 +24,6 @@ export default {
flexDirection: 'row',
height: 56,
justifyContent: 'center',
marginBottom: BaseTheme.spacing[0],
paddingBottom: BaseTheme.spacing[4],
width: '100%'
},

View File

@@ -4,7 +4,6 @@ import { WithTranslation } from 'react-i18next';
import {
ActivityIndicator,
FlatList,
SafeAreaView,
TouchableOpacity,
View,
ViewStyle
@@ -513,7 +512,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
*/
_renderShareMeetingButton() {
return (
<SafeAreaView
<View
style = { [
styles.bottomBar as ViewStyle,
this.state.bottomPadding ? styles.extraBarPadding : null
@@ -524,7 +523,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
src = { IconShare }
style = { styles.shareIcon } />
</TouchableOpacity>
</SafeAreaView>
</View>
);
}

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

@@ -588,7 +588,7 @@ function _registerForNativeEvents(store: IStore) {
}
if (transcription) {
store.dispatch(setRequestingSubtitles(true, false, null));
store.dispatch(setRequestingSubtitles(true, false, null, true));
}
});
@@ -603,7 +603,7 @@ function _registerForNativeEvents(store: IStore) {
}
if (transcription) {
store.dispatch(setRequestingSubtitles(false, false, null));
store.dispatch(setRequestingSubtitles(false, false, null, true));
}
if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {

View File

@@ -13,7 +13,6 @@ export interface INotificationProps {
hideErrorSupportLink?: boolean;
icon?: string;
maxLines?: number;
sticky?: boolean;
title?: string;
titleArguments?: {
[key: string]: string | number;

View File

@@ -27,7 +27,7 @@ const LobbyParticipantList = () => {
}
return (
<>
<View style = { styles.meetingListContainer }>
<View style = { styles.listDetails as ViewStyle } >
<Text style = { styles.lobbyListDescription as TextStyle }>
{ title }
@@ -50,7 +50,7 @@ const LobbyParticipantList = () => {
participant = { p } />)
)
}
</>
</View>
);
};

View File

@@ -94,8 +94,6 @@ function ParticipantItem({
}: IProps) {
const { t } = useTranslation();
const participantNameContainerStyles
= isKnockingParticipant ? styles.lobbyParticipantNameContainer : styles.participantNameContainer;
return (
<View style = { styles.participantContainer as StyleProp<ViewStyle> } >
@@ -106,12 +104,8 @@ function ParticipantItem({
displayName = { displayName }
participantId = { participantID }
size = { 32 } />
<View
style = { [
styles.participantDetailsContainer,
raisedHand && styles.participantDetailsContainerRaisedHand
] as StyleProp<ViewStyle> }>
<View style = { participantNameContainerStyles as StyleProp<ViewStyle> }>
<View>
<View style = { styles.participantNameContainer as StyleProp<ViewStyle> }>
<Text
numberOfLines = { 1 }
style = { styles.participantName as StyleProp<TextStyle> }>
@@ -128,13 +122,11 @@ function ParticipantItem({
</View>
{
!isKnockingParticipant
&& <>
{ raisedHand && <RaisedHandIndicator /> }
<View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
<View>{ AudioStateIcons[audioMediaState] }</View>
</View>
</>
&& <View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
<View>{ raisedHand && <RaisedHandIndicator /> }</View>
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
<View>{ AudioStateIcons[audioMediaState] }</View>
</View>
}
</TouchableOpacity>
{ !local && children }

View File

@@ -6,12 +6,8 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const participantListDescription = {
...BaseTheme.typography.heading6,
color: BaseTheme.palette.text01,
fontSize: 15,
fontWeight: 'bold',
marginLeft: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2],
position: 'relative',
width: '70%'
paddingVertical: BaseTheme.spacing[2]
};
/**
@@ -52,13 +48,6 @@ const contextMenuItem = {
marginLeft: BaseTheme.spacing[3]
};
const participantNameContainer = {
display: 'flex',
flexDirection: 'row',
overflow: 'hidden',
paddingLeft: BaseTheme.spacing[3]
};
/**
* The styles of the native components of the feature {@code participants}.
*/
@@ -91,8 +80,7 @@ export default {
flexDirection: 'row',
height: BaseTheme.spacing[9],
paddingLeft: BaseTheme.spacing[3],
paddingRight: BaseTheme.spacing[3],
width: '100%'
paddingRight: BaseTheme.spacing[3]
},
participantContent: {
@@ -106,24 +94,11 @@ export default {
width: '100%'
},
participantDetailsContainer: {
display: 'flex',
flexDirection: 'column',
width: '73%'
},
participantDetailsContainerRaisedHand: {
width: '65%'
},
participantNameContainer: {
...participantNameContainer,
width: '100%'
},
lobbyParticipantNameContainer: {
...participantNameContainer,
width: '40%'
display: 'flex',
flexDirection: 'row',
overflow: 'hidden',
paddingLeft: BaseTheme.spacing[3],
},
participantName: {
@@ -139,19 +114,19 @@ export default {
},
participantStatesContainer: {
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
marginLeft: 'auto',
width: '15%'
marginLeft: 'auto'
},
participantStateVideo: {
paddingRight: BaseTheme.spacing[3]
marginRight: BaseTheme.spacing[2]
},
raisedHandIndicator: {
backgroundColor: BaseTheme.palette.warning02,
borderRadius: BaseTheme.shape.borderRadius / 2,
borderRadius: BaseTheme.shape.borderRadius,
height: BaseTheme.spacing[4],
width: BaseTheme.spacing[4],
marginLeft: 'auto',
@@ -166,7 +141,7 @@ export default {
buttonAdmit: {
position: 'absolute',
right: 16
right: BaseTheme.spacing[3]
},
buttonReject: {
@@ -201,20 +176,14 @@ export default {
},
participantsPaneFooterContainer: {
alignItems: 'center',
bottom: 0,
height: 128,
left: 0,
paddingHorizontal: BaseTheme.spacing[4],
right: 0
alignSelf: 'stretch',
marginHorizontal: BaseTheme.spacing[5]
},
participantsPaneFooter: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
paddingBottom: BaseTheme.spacing[3],
width: '100%'
marginLeft: 'auto'
},
inviteButton: {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FlatList, Platform, TextInput, View, ViewStyle } from 'react-native';
import { FlatList, SafeAreaView, TextInput, View, ViewStyle } from 'react-native';
import { Divider } from 'react-native-paper';
import { useDispatch } from 'react-redux';
@@ -97,9 +97,6 @@ const PollCreate = (props: AbstractProps) => {
type = { TERTIARY } />
);
const pollCreateButtonsContainerStyles = Platform.OS === 'android'
? pollsStyles.pollCreateButtonsContainerAndroid : pollsStyles.pollCreateButtonsContainerIos;
/* eslint-disable react/jsx-no-bind */
const renderListItem = ({ index }: { index: number; }) => {
@@ -160,7 +157,7 @@ const PollCreate = (props: AbstractProps) => {
), [ question ]);
return (
<View style = { pollsStyles.pollCreateContainer as ViewStyle }>
<SafeAreaView style = { pollsStyles.pollCreateContainer as ViewStyle }>
<View style = { pollsStyles.pollCreateSubContainer as ViewStyle }>
<FlatList
ListHeaderComponent = { renderListHeaderComponent }
@@ -169,7 +166,7 @@ const PollCreate = (props: AbstractProps) => {
keyExtractor = { (item, index) => index.toString() }
ref = { answerListRef }
renderItem = { renderListItem } />
<View style = { pollCreateButtonsContainerStyles as ViewStyle }>
<View style = { pollsStyles.pollCreateButtonsContainer as ViewStyle }>
<Button
accessibilityLabel = 'polls.create.addOption'
disabled = { answers.length >= ANSWERS_LIMIT }
@@ -207,7 +204,7 @@ const PollCreate = (props: AbstractProps) => {
</View>
</View>
</View>
</View>
</SafeAreaView>
);
};

View File

@@ -1,6 +1,5 @@
import { useNavigation } from '@react-navigation/native';
import React, { useEffect } from 'react';
import { Platform } from 'react-native';
import React, { useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
@@ -42,28 +41,27 @@ const PollsPane = (props: AbstractProps) => {
}, [ isPollsTabFocused, unreadPollsCount ]);
const createPollButtonStyles = Platform.OS === 'android'
? pollsStyles.createPollButtonAndroid : pollsStyles.createPollButtonIos;
const renderCreateAPollBtn = useCallback(() => (
!createMode && !isCreatePollsDisabled && <Button
accessibilityLabel = 'polls.create.create'
id = { t('polls.create.create') }
labelKey = 'polls.create.create'
onClick = { onCreate }
style = { pollsStyles.createPollButton }
type = { BUTTON_TYPES.PRIMARY } />
), [ createMode, isCreatePollsDisabled ]);
return (
<JitsiScreen
contentContainerStyle = { pollsStyles.pollPane as StyleType }
disableForcedKeyboardDismiss = { true }
footerComponent = { renderCreateAPollBtn }
hasExtraHeaderHeight = { true }
style = { pollsStyles.pollPaneContainer as StyleType }>
{
createMode
? <PollCreate setCreateMode = { setCreateMode } />
: <>
<PollsList setCreateMode = { setCreateMode } />
{!isCreatePollsDisabled && <Button
accessibilityLabel = 'polls.create.create'
id = { t('polls.create.create') }
labelKey = 'polls.create.create'
onClick = { onCreate }
style = { createPollButtonStyles }
type = { BUTTON_TYPES.PRIMARY } />}
</>
: <PollsList setCreateMode = { setCreateMode } />
}
</JitsiScreen>
);

View File

@@ -158,13 +158,7 @@ export const pollsStyles = createStyleSheet({
marginTop: BaseTheme.spacing[3]
},
pollCreateButtonsContainerAndroid: {
marginBottom: BaseTheme.spacing[8],
marginHorizontal: BaseTheme.spacing[3]
},
pollCreateButtonsContainerIos: {
marginBottom: BaseTheme.spacing[5],
pollCreateButtonsContainer: {
marginHorizontal: BaseTheme.spacing[3]
},
@@ -210,14 +204,8 @@ export const pollsStyles = createStyleSheet({
color: BaseTheme.palette.action01
},
createPollButtonIos: {
marginHorizontal: 20,
marginVertical: BaseTheme.spacing[5]
},
createPollButtonAndroid: {
marginHorizontal: 20,
marginVertical: BaseTheme.spacing[5]
createPollButton: {
marginHorizontal: BaseTheme.spacing[5],
},
pollPane: {

View File

@@ -1,6 +1,5 @@
import React, { ComponentType, PureComponent } from 'react';
import { TouchableWithoutFeedback } from 'react-native';
import { Edge, SafeAreaView } from 'react-native-safe-area-context';
import { TouchableWithoutFeedback, View } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
@@ -9,7 +8,6 @@ import { hideDialog } from '../../../base/dialog/actions';
import { isDialogOpen } from '../../../base/dialog/functions';
import { getParticipantCount } from '../../../base/participants/functions';
import { StyleType } from '../../../base/styles/functions.native';
import { isToolboxVisible } from '../../../toolbox/functions.native';
import ReactionMenu from './ReactionMenu';
@@ -38,11 +36,6 @@ interface IProps {
*/
_styles: StyleType;
/**
* The indicator which determines whether the Toolbox is visible.
*/
_toolboxVisible: boolean;
/**
* The width of the screen.
*/
@@ -87,13 +80,12 @@ class ReactionMenuDialog extends PureComponent<IProps> {
* @returns {ReactElement}
*/
override render() {
const { _height, _participantCount, _styles, _toolboxVisible, _width } = this.props;
const { _height, _participantCount, _styles, _width } = this.props;
return (
<TouchableWithoutFeedback
onPress = { this._onCancel }>
<SafeAreaView
edges = { [ 'bottom', 'left', 'right', !_toolboxVisible && 'top' ].filter(Boolean) as Edge[] }
<View
style = { [
_styles,
{
@@ -103,7 +95,7 @@ class ReactionMenuDialog extends PureComponent<IProps> {
<ReactionMenu
onCancel = { this._onCancel }
overflowMenu = { false } />
</SafeAreaView>
</View>
</TouchableWithoutFeedback>
);
}
@@ -138,8 +130,7 @@ function _mapStateToProps(state: IReduxState) {
_styles: ColorSchemeRegistry.get(state, 'Toolbox').reactionDialog,
_width: state['features/base/responsive-ui'].clientWidth,
_height: state['features/base/responsive-ui'].clientHeight,
_participantCount: getParticipantCount(state),
_toolboxVisible: isToolboxVisible(state)
_participantCount: getParticipantCount(state)
};
}

View File

@@ -37,16 +37,25 @@ export function grantRecordingConsent() {
/**
* Grants recording consent, unmutes audio/video, and closes the dialog.
* Restores the mute state that existed before the consent dialog was shown,
* respecting the user's intentional mute choices from prejoin or initial settings.
*
* @returns {Function}
*/
export function grantRecordingConsentAndUnmute() {
return (dispatch: IStore['dispatch']) => {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
// Read the prejoin mute state from the dialog props stored in Redux
const dialogProps = getState()['features/base/dialog'].componentProps as any;
const audioMuted = dialogProps?.audioWasMuted ?? false;
const videoMuted = dialogProps?.videoWasMuted ?? false;
batch(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
dispatch(setAudioMuted(false, true));
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
// Restore to the mute state before consent was requested.
dispatch(setAudioMuted(audioMuted, false));
dispatch(setVideoMuted(videoMuted, VIDEO_MUTISM_AUTHORITY.USER, false));
dispatch(hideDialog());
});
};

View File

@@ -415,7 +415,7 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
if (this.state.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE
&& this.state.shouldRecordTranscription) {
dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage));
dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage, true));
} else {
_conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: this.state.shouldRecordTranscription

View File

@@ -108,7 +108,8 @@ export default class AbstractStopRecordingDialog<P extends IProps>
}
// TODO: this should be an action in transcribing. -saghul
this.props.dispatch(setRequestingSubtitles(Boolean(_displaySubtitles), _displaySubtitles, _subtitlesLanguage));
this.props.dispatch(
setRequestingSubtitles(Boolean(_displaySubtitles), _displaySubtitles, _subtitlesLanguage, true));
this.props._conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: false

View File

@@ -8,20 +8,21 @@ import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDial
import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../../../../base/media/actions';
import { VIDEO_MUTISM_AUTHORITY } from '../../../../base/media/constants';
import Link from '../../../../base/react/components/native/Link';
import { IRecordingConsentDialogProps } from '../../../reducer';
import styles from '../styles.native';
/**
* Component that renders the dialog for explicit consent for recordings.
*
* @param {IRecordingConsentDialogProps} props - The component props.
* @returns {JSX.Element}
*/
export default function RecordingConsentDialog() {
export default function RecordingConsentDialog({ audioWasMuted = false, videoWasMuted = false }: IRecordingConsentDialogProps) {
const dispatch = useDispatch();
const { t } = useTranslation();
const { recordings } = useSelector((state: IReduxState) => state['features/base/config']);
const { consentLearnMoreLink } = recordings ?? {};
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
@@ -32,11 +33,13 @@ export default function RecordingConsentDialog() {
const consentAndUnmute = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
dispatch(setAudioMuted(false, true));
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
// Restore to the mute state before consent was requested.
dispatch(setAudioMuted(audioWasMuted, false));
dispatch(setVideoMuted(videoWasMuted, VIDEO_MUTISM_AUTHORITY.USER, false));
return true;
}, []);
}, [ audioWasMuted, videoWasMuted ]);
return (
<ConfirmDialog

View File

@@ -10,6 +10,7 @@ import { grantRecordingConsent, grantRecordingConsentAndUnmute } from '../../../
/**
* Component that renders the dialog for explicit consent for recordings.
* The prejoin mute state is read from Redux by the action creator.
*
* @returns {JSX.Element}
*/

View File

@@ -421,12 +421,21 @@ function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['disp
return;
}
// Capture the current mute state BEFORE forcing mute for consent
// This preserves the user's intentional mute choices from prejoin or initial settings
const state = getState();
const audioWasMuted = state['features/base/media'].audio.muted;
const videoWasMuted = state['features/base/media'].video.muted;
batch(() => {
dispatch(markConsentRequested(recorderSession.getID()));
dispatch(setAudioUnmutePermissions(true, true));
dispatch(setVideoUnmutePermissions(true, true));
dispatch(setAudioMuted(true));
dispatch(setVideoMuted(true));
dispatch(openDialog('RecordingConsentDialog', RecordingConsentDialog));
dispatch(openDialog('RecordingConsentDialog', RecordingConsentDialog, {
audioWasMuted,
videoWasMuted
}));
});
}

View File

@@ -42,6 +42,14 @@ export interface IRecordingState {
wasStartRecordingSuggested?: boolean;
}
/**
* Props for the RecordingConsentDialog component.
*/
export interface IRecordingConsentDialogProps {
audioWasMuted?: boolean;
videoWasMuted?: boolean;
}
/**
* The name of the Redux store this feature stores its state in.
*/

View File

@@ -83,7 +83,7 @@ export function toggleRequestingSubtitles() {
* @param {boolean} enabled - The new state of the subtitles.
* @param {boolean} displaySubtitles - Whether to display subtitles or not.
* @param {string} language - The language of the subtitles.
* @param {boolean} backendRecordingOn - Whether backend recording is on.
* @param {boolean} forceBackendRecordingOn - Whether to force that backend recording is on.
* @returns {{
* type: SET_REQUESTING_SUBTITLES,
* backendRecordingOn: boolean,
@@ -95,11 +95,13 @@ export function toggleRequestingSubtitles() {
export function setRequestingSubtitles(
enabled: boolean,
displaySubtitles = true,
language: string | null = `translation-languages:${DEFAULT_LANGUAGE}`) {
language: string | null = `translation-languages:${DEFAULT_LANGUAGE}`,
forceBackendRecordingOn: boolean = false) {
return {
type: SET_REQUESTING_SUBTITLES,
displaySubtitles,
enabled,
forceBackendRecordingOn,
language
};
}

View File

@@ -39,7 +39,6 @@ const useStyles = makeStyles()(theme => {
* Uses the same language options as LanguageSelectorDialog and
* updates the subtitles language preference in Redux.
*
* @param {IProps} props - The component props.
* @returns {JSX.Element} - The rendered component.
*/
function LanguageSelector() {
@@ -51,6 +50,13 @@ function LanguageSelector() {
state,
selectedLanguage?.replace('translation-languages:', '')
));
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
// Hide the "Translate to" option when asyncTranscription is enabled
if (isAsyncTranscriptionEnabled) {
return null;
}
/**
* Maps available languages to Select component options format.

View File

@@ -98,7 +98,7 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case SET_REQUESTING_SUBTITLES:
_requestingSubtitlesChange(store, action.enabled, action.language);
_requestingSubtitlesChange(store, action.enabled, action.language, action.forceBackendRecordingOn);
break;
}
@@ -344,13 +344,16 @@ function _getPrimaryLanguageCode(language: string) {
* @param {Store} store - The redux store.
* @param {boolean} enabled - Whether subtitles should be enabled or not.
* @param {string} language - The language to use for translation.
* @param {boolean} forceBackendRecordingOn - Whether to force backend recording is on or not. This is used only when
* we start recording, stopping is based on whether isTranscribingEnabled is already set.
* @private
* @returns {void}
*/
function _requestingSubtitlesChange(
{ dispatch, getState }: IStore,
enabled: boolean,
language?: string | null) {
language?: string | null,
forceBackendRecordingOn: boolean = false) {
const state = getState();
const { conference } = state['features/base/conference'];
const backendRecordingOn = conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription;
@@ -375,7 +378,9 @@ function _requestingSubtitlesChange(
}));
dispatch(setSubtitlesError(true));
});
} else {
}
if (backendRecordingOn || forceBackendRecordingOn) {
conference?.getMetadataHandler()?.setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: true
});
@@ -388,7 +393,7 @@ function _requestingSubtitlesChange(
language.replace('translation-languages:', ''));
}
if (!enabled && backendRecordingOn
if (!enabled && (backendRecordingOn || forceBackendRecordingOn)
&& conference?.getMetadataHandler()?.getMetadata()[RECORDING_METADATA_ID]?.isTranscribingEnabled) {
conference?.getMetadataHandler()?.setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: false

View File

@@ -76,3 +76,13 @@ local function anonymous(self, message)
end
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
module:hook("pre-resource-unbind", function (e)
local error, session = e.error, e.session;
prosody.events.fire_event('jitsi-pre-session-unbind', {
jid = session.full_jid,
session = session,
error = error
});
end, 11);

View File

@@ -61,6 +61,16 @@ end
module:hook_global("bosh-session", init_session);
module:hook_global("websocket-session", init_session);
module:hook("pre-resource-unbind", function (e)
local error, session = e.error, e.session;
prosody.events.fire_event('jitsi-pre-session-unbind', {
jid = session.full_jid,
session = session,
error = error
});
end, 11);
function provider.test_password(username, password)
return nil, "Password based auth not supported";
end

View File

@@ -13,6 +13,7 @@ local inspect = require 'inspect';
local ban_check_count = module:measure("muc_auth_ban_check", "rate")
local ban_check_users_banned_count = module:measure("muc_auth_ban_users_banned", "rate")
local ban_check_error_count = module:measure("muc_auth_ban_check_error", "rate")
-- we will cache banned tokens to avoid extra requests
-- on destroying session, websocket retries 2 more times before giving up
@@ -68,6 +69,10 @@ local function shouldAllow(session)
cache:set(token, socket.gettime());
end
else
ban_check_error_count();
module:log("warn", "Error code:%s contacting url:%s response:%s request:%s content:%s",
code, ACCESS_MANAGER_URL, response, request, content);
end
end

View File

@@ -28,6 +28,8 @@ end
local jid_node = require 'util.jid'.node;
local jid_host = require 'util.jid'.host;
local jid_split = require 'util.jid'.split;
local jid_resource = require 'util.jid'.resource;
local jid_bare = require 'util.jid'.bare;
local json = require 'cjson.safe';
local st = require 'util.stanza';
local uuid_gen = require 'util.uuid'.generate;
@@ -65,12 +67,16 @@ local main_muc_service;
-- Maps a breakout room jid to the main room jid
local main_rooms_map = {};
-- Maps a full room JID to a bare connection jid for a participant that's changing rooms.
local cache = require 'util.cache';
local switching_room_cache = cache.new(1000);
-- Utility functions
function get_main_room_jid(room_jid)
local _, host = jid_split(room_jid);
return
return
host == main_muc_component_config
and room_jid
or main_rooms_map[room_jid];
@@ -391,7 +397,7 @@ function on_breakout_room_pre_create(event)
end
function on_occupant_joined(event)
local room = event.room;
local occupant, room = event.occupant, event.room;
if is_healthcheck_room(room.jid) then
return;
@@ -409,6 +415,9 @@ function on_occupant_joined(event)
main_room.close_timer:stop();
main_room.close_timer = nil;
end
-- clear any switching state for this occupant, we always store main room / resource
switching_room_cache:set(main_room_jid..'/'..jid_resource(occupant.nick), nil);
end
end
@@ -447,6 +456,11 @@ function on_occupant_pre_leave(event)
prosody.events.fire_event('jitsi-breakout-occupant-leaving', {
room = room; main_room = main_room; occupant = occupant; stanza = stanza; session = session;
});
local presence_status = stanza:get_child_text('status');
if presence_status == 'switch_room' then
switching_room_cache:set(main_room.jid..'/'..jid_resource(occupant.nick), jid_bare(occupant.jid));
end
end
function on_occupant_left(event)
@@ -518,6 +532,86 @@ function on_main_room_destroyed(event)
end
end
-- Checks for a conflict with a JID in the switching_room_cache. In case of a conflict sends an error and returns true (the join is not allowed).
-- in switching_room_cache is the same as the jid that is sending the stanza, if that is the case we can allow
-- the join to proceed by returning false. If there is no match we send an error and return true
-- which should halt the join.
-- @param jid - The jid to check, this is the jid requested to join breakout or main room
-- @param from_bare_jid - The real jid of the occupant trying to join
-- @param room - The room being joined
-- @param stanza - The presence stanza
-- @param origin - The session origin to send error if needed
function check_switching_state(jid, from_bare_jid, room, stanza, origin)
local switching_session_jid = switching_room_cache:get(jid);
if switching_session_jid and switching_session_jid ~= from_bare_jid then
local reply = st.error_reply(stanza, "cancel", "conflict", nil, room.jid):up();
origin.send(reply);
return true;
end
return false;
end
function check_for_existing_occupant_in_room(room, requested_resource, bare_jid, stanza, origin)
local dest_occupant = room:get_occupant_by_nick(room.jid..'/'..requested_resource);
if dest_occupant ~= nil and bare_jid ~= jid_bare(dest_occupant.bare_jid) then
origin.send(st.error_reply(stanza, 'cancel', 'conflict', nil, room.jid):up());
return true;
end
end
-- This is a request to join or change jid in main or breakout room. We need to check whether the requested jid does not
-- conflict with a jid which is currently in switching state or already in another room.
function on_occupant_pre_join_or_change(e)
local room, stanza, origin = e.room, e.stanza, e.origin;
local requested_jid = stanza.attr.to;
local main_room = get_main_room(room.jid);
-- case where the room can be destroyed while someone is switching to it
if not main_room then
origin.send(st.error_reply(stanza, 'cancel', 'service-unavailable'));
return true;
end
local main_room_requested_jid = main_room.jid..'/'..jid_resource(requested_jid);
local bare_jid = jid_bare(stanza.attr.from);
-- we always store main room jid with resource in switching cache
if check_switching_state(main_room_requested_jid, bare_jid, room, stanza, origin) then
return true;
end
if main_room == room then
-- this is the main room we need to check all its breakout rooms
for breakout_room_jid in pairs(room._data.breakout_rooms or {}) do
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
if breakout_room then
if check_for_existing_occupant_in_room(
breakout_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
return true;
end
end
end
else
-- this is a breakout room let's check the main room
if check_for_existing_occupant_in_room(main_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
return true;
end
-- now let's check the rest of the breakout rooms
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
if breakout_room then
if check_for_existing_occupant_in_room(
breakout_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
return true;
end
end
end
end
end
-- Module operations
@@ -544,6 +638,8 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module)
host_module:hook('muc-occupant-left', on_occupant_left);
host_module:hook('muc-room-pre-create', on_breakout_room_pre_create);
host_module:hook('muc-occupant-pre-leave', on_occupant_pre_leave);
host_module:hook('muc-occupant-pre-join', on_occupant_pre_join_or_change);
host_module:hook('muc-occupant-pre-change', on_occupant_pre_join_or_change);
host_module:hook('muc-disco#info', function (event)
local room = event.room;
@@ -641,6 +737,24 @@ process_host_module(breakout_rooms_muc_component_config, function(host_module, h
end
end);
-- clears switching_room_cache on resource unbind (disconnect one way or another)
local function handle_pre_resource_unbind(event)
local participant_bare_jid = jid_bare(event.jid);
-- check switching_room_cache for anyone that is switching rooms but got disconnected so we can clean up the map
local keysToRemove = {};
for key, value in switching_room_cache:items() do
if value == participant_bare_jid then
table.insert(keysToRemove, key)
end
end
for _, key in ipairs(keysToRemove) do
switching_room_cache:set(key, nil);
end
end
module:hook_global('jitsi-pre-session-unbind', handle_pre_resource_unbind);
-- operates on already loaded main muc module
function process_main_muc_loaded(main_muc, host_module)
module:log('debug', 'Main muc loaded');
@@ -650,6 +764,18 @@ function process_main_muc_loaded(main_muc, host_module)
host_module:hook('muc-occupant-joined', on_occupant_joined);
host_module:hook('muc-occupant-left', on_occupant_left);
host_module:hook('muc-room-destroyed', on_main_room_destroyed, 1); -- prosody handles it at 0
host_module:hook('muc-occupant-pre-leave', function(event)
local room, occupant, session, stanza = event.room, event.occupant, event.origin, event.stanza;
local presence_status = stanza:get_child_text('status');
if presence_status == 'switch_room' then
switching_room_cache:set(occupant.nick, jid_bare(occupant.jid));
end
end);
host_module:hook('muc-occupant-pre-join', on_occupant_pre_join_or_change);
host_module:hook('muc-occupant-pre-change', on_occupant_pre_join_or_change);
end
-- process or waits to process the main muc component

View File

@@ -2,6 +2,7 @@ import BasePageObject from './BasePageObject';
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
const AV_MODERATION_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectTitle';
const AV_MODERATION_VIDEO_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectVideoTitle';
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
@@ -57,6 +58,13 @@ export default class Notifications extends BasePageObject {
return this.closeNotification(AV_MODERATION_MUTED_NOTIFICATION_ID, skipNonExisting);
}
/**
* Closes the video muted notification.
*/
async closeAVModerationVideoMutedNotification(skipNonExisting = false) {
return this.closeNotification(AV_MODERATION_VIDEO_MUTED_NOTIFICATION_ID, skipNonExisting);
}
/**
* Closes the ask to unmute notification.
*/
@@ -106,14 +114,22 @@ export default class Notifications extends BasePageObject {
* @private
*/
private async closeNotification(testId: string, skipNonExisting = false) {
const closeButton = this.participant.driver.$('[data-testid="${testId}"] #close-notification');
const closeButton = this.participant.driver.$(`[data-testid="${testId}"] #close-notification`);
if (skipNonExisting && !await closeButton.isExisting()) {
return Promise.resolve();
try {
if (skipNonExisting && !await closeButton.isExisting()) {
return Promise.resolve();
}
await closeButton.moveTo();
await closeButton.click();
} catch (e) {
console.error(`Error closing notification ${testId}`, e);
if (!skipNonExisting) {
throw e;
}
}
await closeButton.moveTo();
await closeButton.click();
}
/**

View File

@@ -40,7 +40,7 @@ export default class PasswordDialog extends BaseDialog {
const passwordInput = this.participant.driver.$(INPUT_KEY_XPATH);
await passwordInput.waitForExist();
await passwordInput.waitForClickable({ timeout: 2000 });
await passwordInput.waitForStable();
await passwordInput.click();
await passwordInput.clearValue();

View File

@@ -45,6 +45,8 @@ export async function unmuteAudioAndCheck(testee: Participant, observer: Partici
* @param observer
*/
export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getNotifications().closeAskToUnmuteNotification(true);
await testee.getNotifications().closeAVModerationVideoMutedNotification(true);
await testee.getToolbar().clickVideoUnmuteButton();
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);

View File

@@ -122,12 +122,12 @@ describe('Audio/video moderation', () => {
await moderatorParticipantsPane.assertVideoMuteIconIsDisplayed(moderator);
await nonModeratorParticipantsPane.assertVideoMuteIconIsDisplayed(nonModerator);
await moderatorParticipantsPane.allowVideo(nonModerator);
await moderatorParticipantsPane.askToUnmute(nonModerator, false);
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
await unmuteAudioAndCheck(nonModerator, p1);
await moderatorParticipantsPane.allowVideo(nonModerator);
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
await unmuteVideoAndCheck(nonModerator, p1);
await moderatorParticipantsPane.clickContextMenuButton();
@@ -190,6 +190,10 @@ describe('Audio/video moderation', () => {
// stop video and check
await p1.getFilmstrip().muteVideo(p2);
// close and open participants pane to make sure the context menu disappears
await p1.getParticipantsPane().close();
await p1.getParticipantsPane().open();
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
@@ -256,11 +260,12 @@ async function unmuteByModerator(
await moderator.getNotifications().waitForRaisedHandNotification();
// ask participant to unmute
await moderatorParticipantsPane.allowVideo(participant);
await moderatorParticipantsPane.askToUnmute(participant, false);
await participant.getNotifications().waitForAskToUnmuteNotification();
await unmuteAudioAndCheck(participant, moderator);
await moderatorParticipantsPane.allowVideo(participant);
await participant.getNotifications().waitForAskToUnmuteNotification();
await unmuteVideoAndCheck(participant, moderator);
if (stopModeration) {

View File

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