Compare commits

...

16 Commits

Author SHA1 Message Date
Mihaela Dumitru
9d4e6c2d0d fix(recording) prevent recording consent dialog for visitors (#16452) 2025-10-08 16:09:55 +03:00
JPL
f3e1fbfdce Update jitsi-meet-rnsdk.podspec
This PR addresses a sporadic issue where cp would fail with a "directory not found" error during file operations. Replaced cp with ditto, which handles directory copying more reliably on macOS and resolves the random failures observed.
2025-10-08 11:15:59 +03:00
damencho
6287c14dd3 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2100.0.0+0d2e5fef...v2101.0.0+8061f52a
2025-10-07 14:09:51 -04:00
bgrozev
8a7ee9bae5 test: Add skip reason to report (#16515)
* test: Add description, skip reason to Allure report.
* test: Adds reasons for skipped tests.
* test: Add more URL normalization test cases.
* ref: Move urlNormalization test to misc/.
2025-10-07 12:28:39 -05:00
Calinteodor
38677dbe0a dep(react-native-immersive-mode): remove everything related to this (#16513)
* Remove anything related to immersive mode on Android. Right now we use top and bottom insets.
2025-10-07 13:50:39 +03:00
bgrozev
1900c42098 test: Fix token for moderation test. (#16510) 2025-10-06 12:40:51 -05:00
Hristo Terezov
6deb0a6385 fix(large-video): Prevents unnecessary updates when container is hidden
Large video was being updated through scheduleLargeVideoUpdate even when
the large video container was hidden via CSS. This occurred in multiple
layout modes: tile view, stage filmstrip (with 2+ participants), and
etherpad editing. These updates caused expensive operations including
setting video streams, managing track listeners, updating avatars, and
running show/hide animations - all wasted CPU cycles since the container
wasn't visible.

The fix introduces a centralized shouldHideLargeVideo() function that checks
all cases where the large video container is hidden. This function is used in
selectParticipantInLargeVideo() to guard to not update the participant id.
A state listener monitors transitions from hidden to visible states and ensures
the large video participant id is properly updated when the container becomes
visible again and set to undefined when large video is hidden.

This improves performance by eliminating unnecessary video element manipulation
and handler execution across all layout modes where large video is not displayed.
2025-10-06 12:00:40 -05:00
Calinteodor
ce567955f0 chore(android): remove api check for setting top bottom insets (#16509)
* chore(android): remove api check for setting top bottom insets
2025-10-06 18:23:20 +03:00
Mihaela Dumitru
9d2f1ce8e0 fix(ui): improve tab badge styling (#16507) 2025-10-06 13:40:41 +03:00
Jaya Allamsetty
841ab8c052 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2099.0.0+89536686...v2100.0.0+0d2e5fef
2025-10-04 07:31:05 -04:00
yanas
3e4f45dc7b Update main-fr.json 2025-10-03 15:25:55 -05:00
damencho
19cff49ab1 feat(conference): Adds option to silently reconnect.
Uses the end_meeting endpoint to trigger a silent reconnect by destroying the current conference and passing its jid.
2025-10-03 14:58:56 -05:00
Mihaela Dumitru
a06c3fe715 feat(file-sharing): show count badges for unread files and notify on uploads/removals (#16484) 2025-10-03 01:52:24 +03:00
Дамян Минков
5580301ef7 fix(prosody): Avoid using stale room instances. (#16492)
* fix(prosody): Avoid using stale room instances.

In very rare cases a participant can request a room and jicofo join there, but the participant don't show up (waiting for host) so jicofo leaves and in the mean time if someone tries to use the room instance just before and after the room is being destroyed, strange things can occur like web connected and joined to a stale room where nothing is received exchanged compared to the live meeting room.

* squash: Revert meeting-id one, will fix it in the problem place where there is an async call.

* squash: Change to a simple check.
2025-10-02 16:50:01 -05:00
damencho
69b0ac4686 fix(tests): Fixes randomly failing start muted test. 2025-10-02 17:18:57 -04:00
bgrozev
9f7eb6b657 test: Add configurable test expectations. (#16496)
* Add a sample "expectations" config.
* feat: Add configurable expectation for dial in.
* Add JaaS unauthenticatedJoins expectation.
* test: Move grantModerator to moderation/, add expectation.
* test: Move kick test to moderation/, fix p2p enabled case.
* test: Add a test case for non-moderator kick.
2025-10-02 15:49:03 -05:00
77 changed files with 738 additions and 502 deletions

View File

@@ -73,7 +73,6 @@ dependencies {
}
implementation project(':react-native-gesture-handler')
implementation project(':react-native-get-random-values')
implementation project(':react-native-immersive-mode')
implementation project(':react-native-keep-awake')
implementation project(':react-native-orientation-locker')
implementation project(':react-native-pager-view')

View File

@@ -96,9 +96,6 @@ public class JitsiMeetActivity extends AppCompatActivity
public static void addTopBottomInsets(@NonNull Window w, @NonNull View v) {
// Only apply if edge-to-edge is supported (API 30+) or enforced (API 35+)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return;
View decorView = w.getDecorView();
decorView.post(() -> {

View File

@@ -101,7 +101,6 @@ class ReactInstanceManagerHolder {
new com.oney.WebRTCModule.WebRTCModulePackage(),
new com.swmansion.gesturehandler.RNGestureHandlerPackage(),
new org.linusu.RNGetRandomValuesPackage(),
new com.rnimmersivemode.RNImmersiveModePackage(),
new com.swmansion.rnscreens.RNScreensPackage(),
new com.zmxv.RNSound.RNSoundPackage(),
new com.th3rdwave.safeareacontext.SafeAreaContextPackage(),

View File

@@ -24,8 +24,6 @@ include ':react-native-giphy'
project(':react-native-giphy').projectDir = new File(rootProject.projectDir, '../node_modules/@giphy/react-native-sdk/android')
include ':react-native-google-signin'
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-google-signin/google-signin/android')
include ':react-native-immersive-mode'
project(':react-native-immersive-mode').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive-mode/android')
include ':react-native-keep-awake'
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/@sayem314/react-native-keep-awake/android')
include ':react-native-orientation-locker'

View File

@@ -120,7 +120,7 @@
"messageAccessibleTitle": "{{user}} dit: ",
"messageAccessibleTitleMe": "Je dis: ",
"messageTo": "Message privé à {{recipient}}",
"messagebox": "Saisissez un message",
"messagebox": "Envoyer un message",
"newMessages": "Nouveaux messages",
"nickname": {
"popover": "Choisissez un pseudonyme",

View File

@@ -570,10 +570,12 @@
"downloadStarted": "File download started",
"dragAndDrop": "Drag and drop files here or anywhere on screen",
"fileAlreadyUploaded": "File has already been uploaded to this meeting.",
"fileRemovedByOther": "Your file '{{ fileName }}' was removed",
"fileTooLargeDescription": "Please make sure the file does not exceed {{ maxFileSize }}.",
"fileTooLargeTitle": "The selected file is too large",
"fileUploadProgress": "File upload progress",
"fileUploadedSuccessfully": "File uploaded successfully",
"newFileNotification": "{{ participantName }} shared '{{ fileName }}'",
"removeFile": "Remove",
"removeFileSuccess": "File removed successfully",
"uploadFailedDescription": "Please try again.",

View File

@@ -158,10 +158,11 @@ const VideoLayout = {
return;
}
const state = APP.store.getState();
const currentContainer = largeVideo.getCurrentContainer();
const currentContainerType = largeVideo.getCurrentContainerType();
const isOnLarge = this.isCurrentlyOnLarge(id);
const state = APP.store.getState();
const participant = getParticipantById(state, id);
const videoTrack = getVideoTrackByParticipant(state, participant);
const videoStream = videoTrack?.jitsiTrack;

23
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/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -87,7 +87,6 @@
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.24.0",
"react-native-get-random-values": "1.11.0",
"react-native-immersive-mode": "https://github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"react-native-orientation-locker": "https://github.com/jitsi/react-native-orientation-locker.git#fe095651d819cf134624f786b61fc8667862178a",
"react-native-pager-view": "6.8.1",
"react-native-paper": "5.10.3",
@@ -18260,8 +18259,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"integrity": "sha512-PCMJIfFWIZtDC6UA/53mT79hiqTGNCRE04/XFgWEr7KRf2QIni2tFh3hW1IPW0OjbtMAkJ1KGQpca/3l6sa5Mw==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.4.6",
@@ -21870,14 +21869,6 @@
"react-native": ">=0.56"
}
},
"node_modules/react-native-immersive-mode": {
"version": "2.0.2",
"resolved": "git+ssh://git@github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.60.5"
}
},
"node_modules/react-native-is-edge-to-edge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
@@ -39715,8 +39706,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"integrity": "sha512-PCMJIfFWIZtDC6UA/53mT79hiqTGNCRE04/XFgWEr7KRf2QIni2tFh3hW1IPW0OjbtMAkJ1KGQpca/3l6sa5Mw==",
"requires": {
"@jitsi/js-utils": "2.4.6",
"@jitsi/logger": "2.1.1",
@@ -42365,10 +42356,6 @@
"fast-base64-decode": "^1.0.0"
}
},
"react-native-immersive-mode": {
"version": "git+ssh://git@github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"from": "react-native-immersive-mode@https://github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c"
},
"react-native-is-edge-to-edge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",

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/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -93,7 +93,6 @@
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.24.0",
"react-native-get-random-values": "1.11.0",
"react-native-immersive-mode": "https://github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"react-native-orientation-locker": "https://github.com/jitsi/react-native-orientation-locker.git#fe095651d819cf134624f786b61fc8667862178a",
"react-native-pager-view": "6.8.1",
"react-native-paper": "5.10.3",

View File

@@ -29,7 +29,7 @@ Pod::Spec.new do |s|
SOURCE_PATH="${PODS_TARGET_SRCROOT}/sounds/"
TARGET_PATH=$(dirname "${CONFIGURATION_BUILD_DIR}")
PROJECT_NAME=$(basename $(dirname $(dirname "${PROJECT_DIR}"))).app
cp -R "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}"
ditto "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}/sounds"
',
}
end

View File

@@ -73,7 +73,6 @@
"react-native-device-info": "0.0.0",
"react-native-get-random-values": "0.0.0",
"react-native-gesture-handler": "0.0.0",
"react-native-immersive-mode": "0.0.0",
"react-native-pager-view": "0.0.0",
"react-native-performance": "0.0.0",
"react-native-orientation-locker": "0.0.0",

View File

@@ -4,7 +4,6 @@ import '../mobile/audio-mode/middleware';
import '../mobile/background/middleware';
import '../mobile/call-integration/middleware';
import '../mobile/external-api/middleware';
import '../mobile/full-screen/middleware';
import '../mobile/navigation/middleware';
import '../mobile/permissions/middleware';
import '../mobile/proximity/middleware';

View File

@@ -2,7 +2,6 @@ import '../mobile/audio-mode/reducer';
import '../mobile/background/reducer';
import '../mobile/call-integration/reducer';
import '../mobile/external-api/reducer';
import '../mobile/full-screen/reducer';
import '../mobile/watchos/reducer';
import '../share-room/reducer';

View File

@@ -52,7 +52,6 @@ import { IMobileAudioModeState } from '../mobile/audio-mode/reducer';
import { IMobileBackgroundState } from '../mobile/background/reducer';
import { ICallIntegrationState } from '../mobile/call-integration/reducer';
import { IMobileExternalApiState } from '../mobile/external-api/reducer';
import { IFullScreenState } from '../mobile/full-screen/reducer';
import { IMobileWatchOSState } from '../mobile/watchos/reducer';
import { INoAudioSignalState } from '../no-audio-signal/reducer';
import { INoiseDetectionState } from '../noise-detection/reducer';
@@ -132,7 +131,6 @@ export interface IReduxState {
'features/file-sharing': IFileSharingState;
'features/filmstrip': IFilmstripState;
'features/follow-me': IFollowMeState;
'features/full-screen': IFullScreenState;
'features/gifs': IGifsState;
'features/google-api': IGoogleApiState;
'features/invite': IInviteState;

View File

@@ -7,6 +7,8 @@ import { showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { determineTranscriptionLanguage } from '../../transcribing/functions';
import { IStateful } from '../app/types';
import { connect } from '../connection/actions';
import { disconnect } from '../connection/actions.any';
import { JitsiTrackErrors } from '../lib-jitsi-meet';
import { setAudioMuted, setVideoMuted } from '../media/actions';
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
@@ -22,7 +24,7 @@ import {
safeDecodeURIComponent
} from '../util/uri';
import { setObfuscatedRoom } from './actions';
import { conferenceWillInit, setObfuscatedRoom } from './actions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
@@ -618,3 +620,34 @@ export function updateTrackMuteState(stateful: IStateful, dispatch: IStore['disp
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
}
/**
* Processes the "destroyed" event of a conference and if the destroyed conference is the current one,
* it silently reconnects to the same room.
*
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
* @param {Function} dispatch - Redux dispatch function.
* @param {Array} params - The parameters for the destroy event.
*
* @returns {boolean} - True if the destroyed conference was the current one, and we are reconnecting, false otherwise.
*/
export function processDestroyConferenceEvent(stateful: IStateful, dispatch: IStore['dispatch'], params: Array<any>) {
const [ jid ] = params;
const conference = getCurrentConference(stateful);
// if the jid of the room is the same as the current conference, we are being
// notified that the current conference has been destroyed, and we need to reconnect
if (conference?.room?.roomjid === jid) {
dispatch(disconnect(true, false))
.then(() => {
dispatch(conferenceWillInit());
logger.info('Dispatching silent re-connect.');
return dispatch(connect());
});
return true;
}
return false;
}

View File

@@ -6,8 +6,8 @@ import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { CONFERENCE_FAILED } from './actionTypes';
import { conferenceLeft } from './actions.native';
import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
import './middleware.any';
import { processDestroyConferenceEvent } from './functions';
MiddlewareRegistry.register(store => next => action => {
const { dispatch } = store;
@@ -23,6 +23,10 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
if (processDestroyConferenceEvent(state, dispatch, error.params)) {
break;
}
if (!notifyOnConferenceDestruction) {
dispatch(conferenceLeft(action.conference));
dispatch(appNavigate(undefined));

View File

@@ -24,8 +24,8 @@ import {
KICKED_OUT
} from './actionTypes';
import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
import { processDestroyConferenceEvent } from './functions';
import logger from './logger';
import './middleware.any';
let screenLock: WakeLockSentinel | undefined;
@@ -127,6 +127,11 @@ MiddlewareRegistry.register(store => next => action => {
const state = getState();
const { notifyOnConferenceDestruction = true } = state['features/base/config'];
const [ reason ] = action.error.params;
if (processDestroyConferenceEvent(state, dispatch, action.error.params)) {
break;
}
const titlekey = Object.keys(TRIGGER_READY_TO_CLOSE_REASONS)[
Object.values(TRIGGER_READY_TO_CLOSE_REASONS).indexOf(reason)
];

View File

@@ -385,10 +385,10 @@ function _propertiesUpdate(properties: object) {
* Closes connection.
*
* @param {boolean} isRedirect - Indicates if the action has been dispatched as part of visitor promotion.
*
* @param {boolean} shouldLeave - Indicates whether to call JitsiConference.leave().
* @returns {Function}
*/
export function disconnect(isRedirect?: boolean) {
export function disconnect(isRedirect?: boolean, shouldLeave = true) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<void> => {
const state = getState();
@@ -407,20 +407,26 @@ export function disconnect(isRedirect?: boolean) {
// intention to leave the conference.
dispatch(conferenceWillLeave(conference_, isRedirect));
promise
= conference_.leave()
.catch((error: Error) => {
logger.warn(
'JitsiConference.leave() rejected with:',
error);
if (!shouldLeave) {
// we are skipping JitsiConference.leave(), but will still dispatch the normal leave flow events
dispatch(conferenceLeft(conference_));
promise = Promise.resolve();
} else {
promise
= conference_.leave()
.catch((error: Error) => {
logger.warn(
'JitsiConference.leave() rejected with:',
error);
// The library lib-jitsi-meet failed to make the
// JitsiConference leave. Which may be because
// JitsiConference thinks it has already left.
// Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
// The library lib-jitsi-meet failed to make the
// JitsiConference leave. Which may be because
// JitsiConference thinks it has already left.
// Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
}
} else {
promise = Promise.resolve();
}

View File

@@ -78,12 +78,6 @@ export const CHAT_ENABLED = 'chat.enabled';
*/
export const FILMSTRIP_ENABLED = 'filmstrip.enabled';
/**
* Flag indicating if fullscreen (immersive) mode should be enabled.
* Default: enabled (true).
*/
export const FULLSCREEN_ENABLED = 'fullscreen.enabled';
/**
* Flag indicating if the Help button should be enabled.
* Default: enabled (true).

View File

@@ -71,11 +71,16 @@ const useStyles = makeStyles()(theme => {
badge: {
...theme.typography.labelBold,
color: theme.palette.text04,
padding: `0 ${theme.spacing(1)}`,
borderRadius: '100%',
alignItems: 'center',
backgroundColor: theme.palette.warning01,
marginLeft: theme.spacing(2)
borderRadius: theme.spacing(2),
color: theme.palette.text04,
display: 'inline-flex',
height: theme.spacing(3),
justifyContent: 'center',
marginLeft: theme.spacing(2),
minWidth: theme.spacing(2),
padding: `0 ${theme.spacing(1)}`
},
icon: {

View File

@@ -7,7 +7,9 @@ import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
import { getUnreadPollCount } from '../../../polls/functions';
import { closeChat, sendMessage } from '../../actions.native';
import { getUnreadFilesCount } from '../../functions';
import { IChatProps as AbstractProps } from '../../types';
import ChatInputBar from './ChatInputBar';
@@ -17,6 +19,21 @@ import styles from './styles';
interface IProps extends AbstractProps {
/**
* The number of unread file messages.
*/
_nbUnreadFiles: number;
/**
* The number of unread messages.
*/
_nbUnreadMessages: number;
/**
* The number of unread polls.
*/
_nbUnreadPolls: number;
/**
* Default prop for navigating between screen components(React Navigation).
*/
@@ -96,7 +113,9 @@ class Chat extends Component<IProps> {
* @private
* @returns {{
* _messages: Array<Object>,
* _nbUnreadMessages: number
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _nbUnreadFiles: number
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
@@ -104,13 +123,16 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
return {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: getUnreadPollCount(state),
_nbUnreadFiles: getUnreadFilesCount(state)
};
}
export default translate(connect(_mapStateToProps)((props: IProps) => {
const { _nbUnreadMessages, dispatch, navigation, t } = props;
const unreadMessagesNr = _nbUnreadMessages > 0;
const { _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles, dispatch, navigation, t } = props;
const totalUnread = _nbUnreadMessages + _nbUnreadPolls + _nbUnreadFiles;
const unreadMessagesNr = totalUnread > 0;
const isFocused = useIsFocused();
@@ -121,14 +143,14 @@ export default translate(connect(_mapStateToProps)((props: IProps) => {
activeUnreadNr = { unreadMessagesNr }
isFocused = { isFocused }
label = { t('chat.tabs.chat') }
nbUnread = { _nbUnreadMessages } />
nbUnread = { totalUnread } />
)
});
return () => {
isFocused && dispatch(closeChat());
};
}, [ isFocused, _nbUnreadMessages ]);
}, [ isFocused, _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles ]);
return (
<Chat { ...props } />

View File

@@ -10,7 +10,7 @@ import { arePollsDisabled } from '../../../conference/functions.any';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { getUnreadPollCount } from '../../../polls/functions';
import { getUnreadCount } from '../../functions';
import { getUnreadCount, getUnreadFilesCount } from '../../functions';
interface IProps extends AbstractButtonProps {
@@ -70,9 +70,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
return {
_isPollsDisabled: arePollsDisabled(state),
// The toggled icon should also be available for new polls
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state),
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state) || getUnreadFilesCount(state),
visible
};
}

View File

@@ -73,6 +73,11 @@ interface IProps extends AbstractProps {
*/
_isResizing: boolean;
/**
* Number of unread file sharing messages.
*/
_nbUnreadFiles: number;
/**
* Number of unread poll messages.
*/
@@ -218,6 +223,7 @@ const Chat = ({
_messages,
_nbUnreadMessages,
_nbUnreadPolls,
_nbUnreadFiles,
_showNamePrompt,
_width,
dispatch,
@@ -512,7 +518,7 @@ const Chat = ({
if (_isFileSharingTabEnabled) {
tabs.push({
accessibilityLabel: t('chat.tabs.fileSharing'),
countBadge: undefined,
countBadge: _focusedTab !== ChatTabs.FILE_SHARING && _nbUnreadFiles > 0 ? _nbUnreadFiles : undefined,
id: ChatTabs.FILE_SHARING,
controlsId: `${ChatTabs.FILE_SHARING}-panel`,
icon: IconShareDoc,
@@ -586,13 +592,14 @@ const Chat = ({
* _messages: Array<Object>,
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _nbUnreadFiles: number,
* _showNamePrompt: boolean,
* _width: number,
* _isResizing: boolean
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, focusedTab, messages, nbUnreadMessages, width, isResizing } = state['features/chat'];
const { isOpen, focusedTab, messages, nbUnreadMessages, nbUnreadFiles, width, isResizing } = state['features/chat'];
const { nbUnreadPolls } = state['features/polls'];
const _localParticipant = getLocalParticipant(state);
@@ -606,6 +613,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: nbUnreadPolls,
_nbUnreadFiles: nbUnreadFiles,
_showNamePrompt: !_localParticipant?.name,
_width: width?.current || CHAT_SIZE,
_isResizing: isResizing

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getUnreadPollCount } from '../../../polls/functions';
import { getUnreadCount } from '../../functions';
import { getUnreadCount, getUnreadFilesCount } from '../../functions';
/**
* The type of the React {@code Component} props of {@link ChatCounter}.
@@ -65,7 +65,7 @@ function _mapStateToProps(state: IReduxState) {
return {
_count: getUnreadCount(state) + getUnreadPollCount(state),
_count: getUnreadCount(state) + getUnreadPollCount(state) + getUnreadFilesCount(state),
_isOpen: isOpen
};

View File

@@ -131,6 +131,16 @@ export function getUnreadCount(state: IReduxState) {
return messagesCount - (lastReadIndex + 1) - reactionMessages;
}
/**
* Gets the unread files count.
*
* @param {IReduxState} state - The redux state.
* @returns {number} The number of unread files.
*/
export function getUnreadFilesCount(state: IReduxState): number {
return state['features/chat']?.nbUnreadFiles || 0;
}
/**
* Get whether the chat smileys are disabled or not.
*

View File

@@ -1,6 +1,7 @@
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
import { ILocalParticipant, IParticipant } from '../base/participants/types';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { ADD_FILE, _FILE_LIST_RECEIVED } from '../file-sharing/actionTypes';
import { IVisitorChatParticipant } from '../visitors/types';
import {
@@ -30,6 +31,7 @@ const DEFAULT_STATE = {
notifyPrivateRecipientsChangedTimestamp: undefined,
reactions: {},
nbUnreadMessages: 0,
nbUnreadFiles: 0,
privateMessageRecipient: undefined,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false,
@@ -53,6 +55,7 @@ export interface IChatState {
name: string;
} | ILocalParticipant;
messages: IMessage[];
nbUnreadFiles: number;
nbUnreadMessages: number;
notifyPrivateRecipientsChangedTimestamp?: number;
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
@@ -235,7 +238,8 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
return {
...state,
focusedTab: action.tabId,
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages,
nbUnreadFiles: action.tabId === ChatTabs.FILE_SHARING ? 0 : state.nbUnreadFiles
};
case SET_CHAT_WIDTH: {
@@ -271,6 +275,23 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
...state,
notifyPrivateRecipientsChangedTimestamp: action.payload
};
case ADD_FILE:
return {
...state,
nbUnreadFiles: action.shouldIncrementUnread ? state.nbUnreadFiles + 1 : state.nbUnreadFiles
};
case _FILE_LIST_RECEIVED: {
const remoteFilesCount = Object.values(action.files).filter(
(file: any) => file.authorParticipantId !== action.localParticipantId
).length;
return {
...state,
nbUnreadFiles: remoteFilesCount
};
}
}
return state;

View File

@@ -3,9 +3,7 @@ import React, { useCallback } from 'react';
import {
BackHandler,
NativeModules,
Platform,
SafeAreaView,
StatusBar,
View,
ViewStyle
} from 'react-native';
@@ -16,8 +14,6 @@ import { appNavigate } from '../../../app/actions.native';
import { IReduxState, IStore } from '../../../app/types';
import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
import { isDisplayNameVisible } from '../../../base/config/functions.native';
import { FULLSCREEN_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import Container from '../../../base/react/components/native/Container';
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
import TintedView from '../../../base/react/components/native/TintedView';
@@ -96,11 +92,6 @@ interface IProps extends AbstractProps {
*/
_filmstripVisible: boolean;
/**
* The indicator which determines whether fullscreen (immersive) mode is enabled.
*/
_fullscreenEnabled: boolean;
/**
* The indicator which determines if the display name is visible.
*/
@@ -277,7 +268,6 @@ class Conference extends AbstractConference<IProps, State> {
override render() {
const {
_brandingStyles,
_fullscreenEnabled
} = this.props;
return (
@@ -287,13 +277,6 @@ class Conference extends AbstractConference<IProps, State> {
_brandingStyles
] }>
<BrandingImageBackground />
{
Platform.OS === 'android'
&& <StatusBar
barStyle = 'light-content'
hidden = { _fullscreenEnabled }
translucent = { _fullscreenEnabled } />
}
{ this._renderContent() }
</Container>
);
@@ -590,7 +573,6 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_calendarEnabled: isCalendarEnabled(state),
_connecting: isConnecting(state),
_filmstripVisible: isFilmstripVisible(state),
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
_isDisplayNameVisible: isDisplayNameVisible(state),
_isParticipantsPaneOpen: isOpen,
_largeVideoParticipantId: state['features/large-video'].participantId,

View File

@@ -39,12 +39,14 @@ export function updateFileProgress(fileId: string, progress: number) {
* Add a file.
*
* @param {IFileMetadata} file - The file to add to the state.
* @param {boolean} shouldIncrementUnread - Whether to increment the unread count.
* @returns {Object}
*/
export function addFile(file: IFileMetadata) {
export function addFile(file: IFileMetadata, shouldIncrementUnread = false) {
return {
type: ADD_FILE,
file
file,
shouldIncrementUnread
};
}

View File

@@ -6,8 +6,10 @@ import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { ChatTabs } from '../chat/constants';
import { showErrorNotification, showNotification, showSuccessNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
import { I_AM_VISITOR_MODE } from '../visitors/actionTypes';
import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
import { addFile, removeFile, updateFileProgress } from './actions';
@@ -23,12 +25,40 @@ import { downloadFile } from './utils';
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch }, previousConference) => {
(conference, { dispatch, getState }, previousConference) => {
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_ADDED, (file: IFileMetadata) => {
dispatch(addFile(file));
const state = getState();
const localParticipant = getLocalParticipant(state);
const isRemoteFile = file.authorParticipantId !== localParticipant?.id;
const { isOpen, focusedTab } = state['features/chat'];
const isFileSharingTabVisible = isOpen && focusedTab === ChatTabs.FILE_SHARING;
dispatch(addFile(file, isRemoteFile && !isFileSharingTabVisible));
if (isRemoteFile && !isFileSharingTabVisible) {
dispatch(showNotification({
titleKey: 'fileSharing.newFileNotification',
titleArguments: { participantName: file.authorParticipantName, fileName: file.fileName }
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_REMOVED, (fileId: string) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
const { files } = state['features/file-sharing'];
const { isOpen, focusedTab } = state['features/chat'];
const removedFile = files.get(fileId);
const isFileSharingTabVisible = isOpen && focusedTab === ChatTabs.FILE_SHARING;
if (removedFile && removedFile.authorParticipantId === localParticipant?.id && !isFileSharingTabVisible) {
dispatch(showNotification({
titleKey: 'fileSharing.fileRemovedByOther',
titleArguments: { fileName: removedFile.fileName },
appearance: NOTIFICATION_TYPE.WARNING
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}
dispatch({
type: _FILE_REMOVED,
fileId
@@ -36,9 +66,13 @@ StateListenerRegistry.register(
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
dispatch({
type: _FILE_LIST_RECEIVED,
files
files,
localParticipantId: localParticipant?.id
});
});
}
@@ -52,6 +86,17 @@ StateListenerRegistry.register(
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case I_AM_VISITOR_MODE: {
if (!action.iAmVisitor) {
const state = store.getState();
const conference = getCurrentConference(state);
conference?.getFileSharing()?.requestFileList?.();
}
return next(action);
}
case UPLOAD_FILES: {
const state = store.getState();
const conference = getCurrentConference(state);

View File

@@ -1,6 +1,11 @@
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { ADD_FILE, UPDATE_FILE_UPLOAD_PROGRESS, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
import {
ADD_FILE,
UPDATE_FILE_UPLOAD_PROGRESS,
_FILE_LIST_RECEIVED,
_FILE_REMOVED
} from './actionTypes';
import { IFileMetadata } from './types';
export interface IFileSharingState {
@@ -20,6 +25,7 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.set(action.file.fileId, action.file);
return {
...state,
files: newFiles
};
}
@@ -30,6 +36,7 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.delete(action.fileId);
return {
...state,
files: newFiles
};
}
@@ -43,12 +50,14 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
}
return {
...state,
files: newFiles
};
}
case _FILE_LIST_RECEIVED: {
return {
...state,
files: new Map(Object.entries(action.files))
};
}

View File

@@ -11,7 +11,6 @@ import {
getVirtualScreenshareParticipantByOwnerId
} from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { getAutoPinSetting } from '../video-layout/functions';
import {
@@ -19,6 +18,7 @@ import {
SET_LARGE_VIDEO_DIMENSIONS,
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
} from './actionTypes';
import { shouldHideLargeVideo } from './functions';
/**
* Action to select the participant to be displayed in LargeVideo based on the
@@ -34,12 +34,8 @@ export function selectParticipantInLargeVideo(participant?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
if (isStageFilmstripAvailable(state, 2)) {
return;
}
// Keep Etherpad open.
if (state['features/etherpad'].editing) {
// Skip large video updates when the large video container is hidden.
if (shouldHideLargeVideo(state)) {
return;
}

View File

@@ -1,5 +1,7 @@
import { IReduxState } from '../app/types';
import { getParticipantById } from '../base/participants/functions';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { shouldDisplayTileView } from '../video-layout/functions.any';
/**
* Selector for the participant currently displaying on the large video.
@@ -12,3 +14,17 @@ export function getLargeVideoParticipant(state: IReduxState) {
return getParticipantById(state, participantId ?? '');
}
/**
* Determines whether the large video container should be hidden.
* Large video is hidden in tile view, stage filmstrip mode (with multiple participants),
* or when editing etherpad.
*
* @param {IReduxState} state - The Redux state.
* @returns {boolean} True if large video should be hidden, false otherwise.
*/
export function shouldHideLargeVideo(state: IReduxState): boolean {
return shouldDisplayTileView(state)
|| isStageFilmstripAvailable(state, 2)
|| Boolean(state['features/etherpad']?.editing);
}

View File

@@ -0,0 +1,26 @@
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { SELECT_LARGE_VIDEO_PARTICIPANT } from './actionTypes';
import { selectParticipantInLargeVideo } from './actions.any';
import { shouldHideLargeVideo } from './functions';
/**
* Updates the large video when transitioning from a hidden state to visible state.
* This ensures the large video is properly updated when exiting tile view, stage filmstrip,
* whiteboard, or etherpad editing modes.
*/
StateListenerRegistry.register(
/* selector */ state => shouldHideLargeVideo(state),
/* listener */ (isHidden, { dispatch }) => {
// When transitioning from hidden to visible state, select participant (because currently it is undefined).
// Otherwise set it to undefined because we don't show the large video.
if (!isHidden) {
dispatch(selectParticipantInLargeVideo());
} else {
dispatch({
type: SELECT_LARGE_VIDEO_PARTICIPANT,
participantId: undefined
});
}
}
);

View File

@@ -0,0 +1 @@
import './subscriber.any';

View File

@@ -4,6 +4,7 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { getVideoTrackByParticipant } from '../base/tracks/functions.web';
import { getLargeVideoParticipant } from './functions';
import './subscriber.any';
/**
* Updates the on stage participant video.

View File

@@ -1,12 +0,0 @@
/**
* The type of (redux) action to set the react-native-immersive's change event
* subscription.
*
* {
* type: _SET_IMMERSIVE_SUBSCRIPTION,
* subscription: Function
* }
*
* @protected
*/
export const _SET_IMMERSIVE_SUBSCRIPTION = '_SET_IMMERSIVE_SUBSCRIPTION';

View File

@@ -1,21 +0,0 @@
import { NativeEventSubscription } from 'react-native';
import { _SET_IMMERSIVE_SUBSCRIPTION } from './actionTypes';
/**
* Sets the change event listener to be used with react-native-immersive's API.
*
* @param {Function} subscription - The function to be used with
* react-native-immersive's API as the change event listener.
* @protected
* @returns {{
* type: _SET_IMMERSIVE_SUBSCRIPTION,
* subscription: ?NativeEventSubscription
* }}
*/
export function _setImmersiveSubscription(subscription?: NativeEventSubscription) {
return {
type: _SET_IMMERSIVE_SUBSCRIPTION,
subscription
};
}

View File

@@ -1,22 +0,0 @@
import { IReduxState } from '../../app/types';
import { getCurrentConference } from '../../base/conference/functions';
import { isAnyDialogOpen } from '../../base/dialog/functions';
import { FULLSCREEN_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { isLocalVideoTrackDesktop } from '../../base/tracks/functions.any';
/**
* Checks whether full-screen state should be used or not.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - Whether full-screen state should be used or not.
*/
export function shouldUseFullScreen(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const conference = getCurrentConference(state);
const dialogOpen = isAnyDialogOpen(state);
const fullscreenEnabled = getFeatureFlag(state, FULLSCREEN_ENABLED, true);
const isDesktopSharing = isLocalVideoTrackDesktop(state);
return conference ? !audioOnly && !dialogOpen && !isDesktopSharing && fullscreenEnabled : false;
}

View File

@@ -1,3 +0,0 @@
import { getLogger } from '../../base/logging/functions';
export default getLogger('mobile-app:full-screen');

View File

@@ -1,102 +0,0 @@
import ImmersiveMode from 'react-native-immersive-mode';
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
import { _setImmersiveSubscription } from './actions';
import { shouldUseFullScreen } from './functions';
import logger from './logger';
type BarVisibilityType = {
navigationBottomBar: boolean;
statusBar: boolean;
};
type ImmersiveListener = (visibility: BarVisibilityType) => void;
/**
* Middleware that captures conference actions and activates or deactivates the
* full screen mode. On iOS it hides the status bar, and on Android it uses the
* immersive mode:
* https://developer.android.com/training/system-ui/immersive.html
* In immersive mode the status and navigation bars are hidden and thus the
* entire screen will be covered by our application.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_WILL_MOUNT: {
_setImmersiveListener(store, _onImmersiveChange.bind(undefined, store));
break;
}
case APP_WILL_UNMOUNT:
_setImmersiveListener(store, undefined);
break;
}
return next(action);
});
StateListenerRegistry.register(
/* selector */ shouldUseFullScreen,
/* listener */ fullScreen => _setFullScreen(fullScreen)
);
/**
* Handler for Immersive mode changes. This will be called when Android's
* immersive mode changes. This can happen without us wanting, so re-evaluate if
* immersive mode is desired and reactivate it if needed.
*
* @param {Object} store - The redux store.
* @private
* @returns {void}
*/
function _onImmersiveChange({ getState }: IStore) {
const state = getState();
const { appState } = state['features/mobile/background'];
if (appState === 'active') {
_setFullScreen(shouldUseFullScreen(state));
}
}
/**
* Activates/deactivates the full screen mode. On iOS it will hide the status
* bar, and on Android it will turn immersive mode on.
*
* @param {boolean} fullScreen - True to set full screen mode, false to
* deactivate it.
* @private
* @returns {void}
*/
function _setFullScreen(fullScreen: boolean) {
logger.info(`Setting full-screen mode: ${fullScreen}`);
ImmersiveMode.fullLayout(fullScreen);
ImmersiveMode.setBarMode(fullScreen ? 'Full' : 'Normal');
}
/**
* Notifies the feature filmstrip that the action
* {@link _SET_IMMERSIVE_LISTENER} is being dispatched within a specific redux
* store.
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Function} listener - Listener for immersive state.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setImmersiveListener({ dispatch, getState }: IStore, listener?: ImmersiveListener) {
const { subscription } = getState()['features/full-screen'];
subscription?.remove();
dispatch(_setImmersiveSubscription(listener ? ImmersiveMode.addEventListener(listener) : undefined));
}

View File

@@ -1,21 +0,0 @@
import { NativeEventSubscription } from 'react-native';
import ReducerRegistry from '../../base/redux/ReducerRegistry';
import { _SET_IMMERSIVE_SUBSCRIPTION } from './actionTypes';
export interface IFullScreenState {
subscription?: NativeEventSubscription;
}
ReducerRegistry.register<IFullScreenState>('features/full-screen', (state = {}, action): IFullScreenState => {
switch (action.type) {
case _SET_IMMERSIVE_SUBSCRIPTION:
return {
...state,
subscription: action.subscription
};
}
return state;
});

View File

@@ -13,6 +13,7 @@ import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functio
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
import { canAddTranscriber, isRecorderTranscriptionsRunning } from '../transcribing/functions';
import { iAmVisitor } from '../visitors/functions';
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
import {
@@ -444,10 +445,11 @@ export function shouldRequireRecordingConsent(recorderSession: any, state: IRedu
= state['features/dynamic-branding'] || {};
const { conference } = state['features/base/conference'] || {};
const { requireConsent, skipConsentInMeeting } = state['features/base/config'].recordings || {};
const _iAmVisitor = iAmVisitor(state);
const { iAmRecorder, testing: { showSpotConsentDialog = false } = {} } = state['features/base/config'];
const { consentRequested } = state['features/recording'];
if (iAmRecorder) {
if (iAmRecorder || _iAmVisitor) {
return false;
}

View File

@@ -6,6 +6,7 @@ module:set_global();
local util = module:require "util";
local async_handler_wrapper = util.async_handler_wrapper;
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local get_room_from_jid = util.get_room_from_jid;
local starts_with = util.starts_with;
@@ -17,6 +18,8 @@ local parse = neturl.parseQuery;
local token_util;
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
local muc_domain = muc_domain_prefix..'.'..muc_domain_base;
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
@@ -47,6 +50,7 @@ function handle_terminate_meeting (event)
end
local params = parse(event.request.url.query);
local conference = params["conference"];
local silent_reconnect = params['silent-reconnect'];
local room_jid;
if conference then
@@ -78,8 +82,13 @@ function handle_terminate_meeting (event)
module:log("warn", "Room not found")
return { status_code = 404 };
else
module:log("info", "Destroy room jid %s", room.jid)
room:destroy(nil, "The meeting has been terminated")
if silent_reconnect == 'true' then
module:log('info', 'Setting silent_reconnect on room %s', room.jid);
room:destroy(internal_room_jid_match_rewrite(room.jid), 'The meeting has been terminated silently')
else
module:log("info", "Destroy room jid %s", room.jid)
room:destroy(nil, "The meeting has been terminated")
end
end
event_count_success()
return { status_code = 200 };

View File

@@ -48,6 +48,7 @@ local NOTIFY_LOBBY_ACCESS_DENIED = 'LOBBY-ACCESS-DENIED';
local util = module:require "util";
local ends_with = util.ends_with;
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
local get_room_from_jid = util.get_room_from_jid;
local is_healthcheck_room = util.is_healthcheck_room;
local presence_check_status = util.presence_check_status;
local process_host_module = util.process_host_module;
@@ -105,7 +106,7 @@ end
-- Sends a json message notifying that the jid was granted/denied access in lobby
-- the message from is the actor that did the operation
function notify_lobby_access(room, actor, jid, display_name, granted)
function notify_lobby_access(room_jid, actor, jid, display_name, granted)
local notify_json = {
value = jid,
name = display_name
@@ -116,6 +117,12 @@ function notify_lobby_access(room, actor, jid, display_name, granted)
notify_json.event = NOTIFY_LOBBY_ACCESS_DENIED;
end
local room = get_room_from_jid(room_jid);
if not room then
module:log('error', 'Room not found for %s', room_jid)
return;
end
broadcast_json_msg(room, actor, notify_json);
end
@@ -227,7 +234,7 @@ function attach_lobby_room(room, actor)
-- avoid lobby destroy while it is enabled
new_room:set_persistent(true);
module:log("info","Lobby room jid = %s created from:%s", lobby_room_jid, actor);
new_room.main_room = room;
new_room.main_room_jid = room.jid;
room._data.lobbyroom = new_room.jid;
room:save(true);
return true
@@ -245,6 +252,8 @@ function destroy_lobby_room(room, newjid, message)
if lobby_room_obj then
lobby_room_obj:set_persistent(false);
lobby_room_obj:destroy(newjid, message);
module:log('info', 'Lobby room destroyed %s', lobby_room_obj.jid)
end
room._data.lobbyroom = nil;
room._data.lobby_extra_reason = nil;
@@ -412,13 +421,18 @@ function process_lobby_muc_loaded(lobby_muc, host_module)
local room_mt = lobby_muc_service.room_mt;
-- we base affiliations (roles) in lobby muc component to be based on the roles in the main muc
room_mt.get_affiliation = function(room, jid)
if not room.main_room then
if not room.main_room_jid then
module:log('error', 'No main room(%s) for %s!', room.jid, jid);
return 'none';
end
-- moderators in main room are moderators here
local role = room.main_room.get_affiliation(room.main_room, jid);
local main_room = get_room_from_jid(room.main_room_jid);
if not main_room then
module:log('error', 'Main room not found for %s!', room.main_room_jid);
return 'none';
end
local role = main_room.get_affiliation(main_room, jid);
if role then
return role;
end
@@ -433,7 +447,7 @@ function process_lobby_muc_loaded(lobby_muc, host_module)
local display_name = occupant:get_presence():get_child_text(
'nick', 'http://jabber.org/protocol/nick');
-- we need to notify in the main room
notify_lobby_access(room.main_room, actor, occupant.nick, display_name, false);
notify_lobby_access(room.main_room_jid, actor, occupant.nick, display_name, false);
end
end);
end
@@ -589,7 +603,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
local display_name = occupant:get_presence():get_child_text(
'nick', 'http://jabber.org/protocol/nick');
notify_lobby_access(room, from, occupant.nick, display_name, true);
notify_lobby_access(room.jid, from, occupant.nick, display_name, true);
end
end
end

View File

@@ -108,7 +108,13 @@ module:hook("muc-occupant-pre-join", function (event)
if not room.join_rate_queue_timer then
timer.add_task(1, function ()
local status, result = pcall(timer_process_queue_elements,
if room.destroying then
-- if room was destroyed in the mean time, ignore
return;
end
local status, result = pcall(
timer_process_queue_elements,
join_rate_per_conference,
room.join_rate_presence_queue,
function(ev)

View File

@@ -2,6 +2,10 @@
* An interface that tests can export (as a TEST_PROPERTIES property) to define what they require.
*/
export type ITestProperties = {
/**
* A more detailed description of what the test does, to be included in the Allure report.
*/
description?: string;
/** The test requires jaas, it should be skipped when the jaas configuration is not enabled. */
useJaas: boolean;
/** The test requires the webhook proxy. */

View File

@@ -4,18 +4,7 @@
export const config = {
/** Enable debug logging. Note this includes private information from .env */
debug: Boolean(process.env.JITSI_DEBUG?.trim()),
/** Whether to expect the environment to automatically elect a new moderator when the existing moderator leaves. */
autoModerator: (() => {
if (typeof process.env.AUTO_MODERATOR !== 'undefined') {
return process.env.AUTO_MODERATOR?.trim() === 'true';
}
// If not explicitly configured, fallback to recognizing whether we're running against one of the JaaS
// environments which are known to have the setting disabled.
return !Boolean(
process.env.JWT_PRIVATE_KEY_PATH && process.env.JWT_KID?.startsWith('vpaas-magic-cookie-')
);
})(),
expectationsFile: process.env.EXPECTATIONS?.trim(),
jaas: {
customerId: (() => {
if (typeof process.env.JAAS_TENANT !== 'undefined') {

View File

@@ -0,0 +1,50 @@
import fs from 'fs';
import { merge } from 'lodash-es';
import { config } from './TestsConfig';
const defaultExpectations = {
dialIn: {
/*
* The dial-in functionality is enabled.
* true -> assert the config is enabled, the UI elements are displayed, and the feature works.
* false -> assert the config is disabled and the UI elements are not displayed.
* null -> if the config is enabled, assert the UI elements are displayed and the feature works.
*/
enabled: null,
},
jaas: {
/**
* Whether the jaas account is configured with the account-level setting to allow unauthenticated users to join.
*/
unauthenticatedJoins: false
},
moderation: {
// Everyone is a moderator.
allModerators: false,
// When a moderator leaves, another one is elected.
autoModerator: true,
// The first to join is a moderator.
firstModerator: true,
// The grantOwner function is available.
grantModerator: true
}
};
let overrides: any = {};
if (config.expectationsFile) {
try {
const str = fs.readFileSync(config.expectationsFile, 'utf8');
// Remove comments and multiline comments.
overrides = JSON.parse(str.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, ''));
} catch (e) {
console.error('Error reading expectations file', e);
}
console.log('Loaded expectations from', config.expectationsFile);
}
export const expectations = merge(defaultExpectations, overrides);
console.log('Expectations:', expectations);

View File

@@ -18,8 +18,9 @@ export type IContext = {
roomName: string;
/**
* A flag that tests can set, which signals to the framework that the (rest of the) test suite should be skipped.
* A string value indicates the reason for the skipped (to be included in the Allure report).
*/
skipSuiteTests: boolean;
skipSuiteTests: boolean | string;
/**
* Test properties provided by the test file via TestProperties.setTestProperties. Used by the framework to
* set up the context appropriately.

View File

@@ -136,6 +136,20 @@ export default class Filmstrip extends BasePageObject {
return await elem.isExisting() ? await elem.getAttribute('src') : null;
}
/**
* Returns true if the endpoint is dominant speaker and false otherwise.
* Uses the dominant-speaker class on the video thumbnail in order to check.
*
* @param {string} endpointId - The endpoint id of the participant we want to check.
* @returns {boolean} - True if the endpoint is dominant speaker and false otherwise.
*/
async isDominantSpeaker(endpointId: string) {
const elem = this.participant.driver.$(
`//span[@id='participant_${endpointId}' and contains(@class,'dominant-speaker')]`);
return await elem.isExisting();
}
/**
* Grants moderator rights to a participant.
* @param participant

View File

@@ -1,5 +1,6 @@
import process from 'node:process';
import { expectations } from '../../helpers/expectations';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
@@ -9,16 +10,22 @@ describe('Fake Dial-In', () => {
// check rest url is not configured
if (process.env.DIAL_IN_REST_URL) {
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set';
return;
}
await ensureOneParticipant();
const configEnabled = await isDialInEnabled(ctx.p1);
if (expectations.dialIn.enabled !== null) {
expect(configEnabled).toBe(expectations.dialIn.enabled);
}
// check dial-in is enabled, so skip
if (await isDialInEnabled(ctx.p1)) {
ctx.skipSuiteTests = true;
if (configEnabled) {
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
}
});

View File

@@ -1,42 +0,0 @@
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
describe('Grant moderator', () => {
it('joining the meeting', async () => {
await ensureOneParticipant();
if (await ctx.p1.execute(() => typeof APP.conference._room.grantOwner !== 'function')) {
ctx.skipSuiteTests = true;
return;
}
await ensureTwoParticipants();
});
it('grant moderator and validate', async () => {
const { p1, p2 } = ctx;
if (!await p1.isModerator()) {
ctx.skipSuiteTests = true;
return;
}
if (await p2.isModerator()) {
ctx.skipSuiteTests = true;
return;
}
await p1.getFilmstrip().grantModerator(p2);
await p2.driver.waitUntil(
() => p2.isModerator(),
{
timeout: 3000,
timeoutMsg: 'p2 did not become moderator'
}
);
});
});

View File

@@ -1,44 +0,0 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Kick', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants();
if (!await ctx.p1.isModerator()) {
ctx.skipSuiteTests = true;
}
});
it('kick and check', () => kickParticipant2AndCheck());
it('kick p2p and check', async () => {
await ensureTwoParticipants({
configOverwrite: {
p2p: {
enabled: true
}
}
});
await kickParticipant2AndCheck();
});
});
/**
* Kicks the second participant and checks that the participant is removed from the conference and that dialog is open.
*/
async function kickParticipant2AndCheck() {
const { p1, p2 } = ctx;
await p1.getFilmstrip().kickParticipant(await p2.getEndpointId());
await p1.waitForParticipants(0);
// check that the kicked participant sees the kick reason dialog
// let's wait for this to appear at least 2 seconds
await p2.driver.waitUntil(
async () => p2.isLeaveReasonDialogOpen(), {
timeout: 2000,
timeoutMsg: 'No leave reason dialog shown for p2'
});
}

View File

@@ -1,41 +0,0 @@
import { multiremotebrowser } from '@wdio/globals';
import { config } from '../../helpers/TestsConfig';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('URL Normalisation', () => {
it('joining the meeting', async () => {
// if we are running with token this becomes ugly to match the URL
if (config.jwt.preconfiguredToken) {
ctx.skipSuiteTests = true;
return;
}
// a hack to extract the baseUrl that the test will use
const baseUrl = multiremotebrowser.getInstance('p1').options.baseUrl;
if (!baseUrl) {
throw new Error('baseUrl is not set');
}
await ensureTwoParticipants({
tenant: 'tenant@example.com',
roomName: `${ctx.roomName}@example.com`
});
});
it('check', async () => {
const currentUrlStr = await ctx.p1.driver.getUrl();
const currentUrl = new URL(currentUrlStr);
const path = currentUrl.pathname;
const parts = path.split('/');
expect(parts[1]).toBe('tenantexample.com');
// @ts-ignore
expect(parts[2]).toBe(`${ctx.roomName}example.com`);
});
});

View File

@@ -61,10 +61,10 @@ async function testActiveSpeaker(
const otherParticipant1Driver = otherParticipant1.driver;
await otherParticipant1Driver.waitUntil(
async () => await otherParticipant1.getLargeVideo().getResource() === speakerEndpoint,
async () => await otherParticipant1.getFilmstrip().isDominantSpeaker(speakerEndpoint),
{
timeout: 30_000, // 30 seconds
timeoutMsg: 'Active speaker not displayed on large video.'
timeoutMsg: `${activeSpeaker.name} is not selected as active speaker.`
});
// just a debug print to go in logs

View File

@@ -1,5 +1,5 @@
import { Participant } from '../../helpers/Participant';
import { config } from '../../helpers/TestsConfig';
import { expectations } from '../../helpers/expectations';
import {
ensureOneParticipant,
ensureThreeParticipants, ensureTwoParticipants,
@@ -17,7 +17,8 @@ describe('AVModeration', () => {
if (!await p1.isModerator()
|| (await p1.isModerator() && await p2.isModerator() && await p3.isModerator())) {
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = `Unsupported moderator configuration: p1=${await p1.isModerator()},\
p2=${await p2.isModerator()}, p3=${await p3.isModerator()}`;
}
});
@@ -79,7 +80,7 @@ describe('AVModeration', () => {
it('hangup and change moderator', async () => {
// The test below is only correct when the environment is configured to automatically elect a new moderator
// when the moderator leaves. For environments where this is not the case, the test is skipped.
if (!config.autoModerator) {
if (!expectations.autoModerator) {
return;
}

View File

@@ -17,7 +17,7 @@ describe('BreakoutRooms', () => {
await ensureTwoParticipants();
if (!await ctx.p1.isBreakoutRoomsSupported()) {
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'The environment does not support breakout rooms.';
}
});

View File

@@ -42,6 +42,7 @@ describe('Codec selection', () => {
it('asymmetric codecs with AV1', async () => {
await ensureThreeParticipants({
configOverwrite: {
disableTileView: true,
videoQuality: {
codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
}
@@ -95,6 +96,7 @@ describe('Codec selection', () => {
await ensureThreeParticipants({
configOverwrite: {
disableTileView: true,
videoQuality: {
codecPreferenceOrder: [ 'VP8' ]
}

View File

@@ -1,5 +1,5 @@
import { P1, P3, Participant } from '../../helpers/Participant';
import { config } from '../../helpers/TestsConfig';
import { expectations } from '../../helpers/expectations';
import {
ensureOneParticipant,
ensureThreeParticipants,
@@ -14,7 +14,7 @@ describe('Lobby', () => {
await ensureOneParticipant();
if (!await ctx.p1.execute(() => APP.conference._room.isLobbySupported())) {
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'The environment does not support lobby.';
}
});
@@ -198,7 +198,7 @@ describe('Lobby', () => {
it('change of moderators in lobby', async () => {
// The test below is only correct when the environment is configured to automatically elect a new moderator
// when the moderator leaves. For environments where this is not the case, the test is skipped.
if (!config.autoModerator) {
if (!expectations.autoModerator) {
return;
}
await hangupAllParticipants();
@@ -291,7 +291,7 @@ describe('Lobby', () => {
it('moderator leaves while lobby enabled', async () => {
// The test below is only correct when the environment is configured to automatically elect a new moderator
// when the moderator leaves. For environments where this is not the case, the test is skipped.
if (!config.autoModerator) {
if (!expectations.autoModerator) {
return;
}
const { p1, p2, p3 } = ctx;

View File

@@ -139,6 +139,9 @@ describe('StartMuted', () => {
await p3.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
// Unmute and see if the audio works
// we need 1 and 2 to be muted so we have a dominant speaker event for correct audio levels calculations
await p1.getToolbar().clickAudioMuteButton();
await p2.getToolbar().clickAudioMuteButton();
await p3.getToolbar().clickAudioUnmuteButton();
p1.log('configOptionsTest, unmuted third participant');
await p1.waitForAudioMuted(p3, false /* unmuted */);

View File

@@ -1,6 +1,7 @@
import process from 'node:process';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { expectations } from '../../helpers/expectations';
import { ensureOneParticipant } from '../../helpers/participants';
import { cleanup, dialIn, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
@@ -8,7 +9,7 @@ describe('Dial-In', () => {
it('join participant', async () => {
// check rest url is configured
if (!process.env.DIAL_IN_REST_URL) {
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
return;
}
@@ -16,7 +17,7 @@ describe('Dial-In', () => {
// This is a temporary hack to avoid failing when running against a jaas env. The same cases are covered in
// jaas/dial/dialin.spec.ts.
if (testsConfig.jaas.enabled) {
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'JaaS is configured.';
return;
}
@@ -25,8 +26,14 @@ describe('Dial-In', () => {
expect(await ctx.p1.isInMuc()).toBe(true);
if (!await isDialInEnabled(ctx.p1)) {
ctx.skipSuiteTests = true;
const configEnabled = await isDialInEnabled(ctx.p1);
if (expectations.dialIn.enabled !== null) {
expect(configEnabled).toBe(expectations.dialIn.enabled);
}
if (!configEnabled) {
ctx.skipSuiteTests = 'The environment does not support dial-in, and no expectation has been set.';
}
});
@@ -37,13 +44,13 @@ describe('Dial-In', () => {
dialInPin = await ctx.p1.getDialInPin();
} catch (e) {
console.error('dial-in.test.no-pin');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'No dial-in pin is available.';
throw e;
}
if (dialInPin.length === 0) {
console.error('dial-in.test.no-pin');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'The dial-in pin is empty.';
throw new Error('no pin');
}

View File

@@ -1,17 +1,17 @@
import { Participant } from '../../helpers/Participant';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { expectations } from '../../helpers/expectations';
import { ensureOneParticipant } from '../../helpers/participants';
import { assertDialInDisplayed, assertUrlDisplayed, isDialInEnabled, verifyMoreNumbersPage } from '../helpers/DialIn';
describe('Invite', () => {
let p1: Participant;
let dialInEnabled: boolean;
it('setup', async () => {
// This is a temporary hack to avoid failing when running against a jaas env. The same cases are covered in
// jaas/dial/dialin.spec.ts.
if (testsConfig.jaas.enabled) {
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'JaaS is configured.';
return;
}
@@ -19,23 +19,30 @@ describe('Invite', () => {
await ensureOneParticipant();
p1 = ctx.p1;
dialInEnabled = await isDialInEnabled(p1);
});
// The URL should always be displayed.
it('url displayed', () => assertUrlDisplayed(p1));
it('dial-in displayed', async () => {
if (!dialInEnabled) {
return;
it('config values', async () => {
const dialInEnabled = await isDialInEnabled(p1);
if (expectations.dialIn.enabled !== null) {
expect(dialInEnabled).toBe(expectations.dialIn.enabled);
}
});
it('dial-in displayed', async () => {
if (expectations.dialIn.enabled !== null) {
await assertDialInDisplayed(p1, expectations.dialIn.enabled);
}
await assertDialInDisplayed(p1);
});
it('view more numbers page', async () => {
if (!dialInEnabled) {
return;
if (expectations.dialIn.enabled === true) {
// TODO: assert the page is NOT shown when the expectation is false.
await verifyMoreNumbersPage(p1);
}
await verifyMoreNumbersPage(p1);
});
});

View File

@@ -95,14 +95,14 @@ export async function assertUrlDisplayed(p: Participant) {
await inviteDialog.waitTillOpen(true);
}
export async function assertDialInDisplayed(p: Participant) {
export async function assertDialInDisplayed(p: Participant, displayed: boolean = false) {
const inviteDialog = p.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
expect((await inviteDialog.getDialInNumber()).length > 0).toBe(true);
expect((await inviteDialog.getPinNumber()).length > 0).toBe(true);
expect((await inviteDialog.getDialInNumber()).length > 0).toBe(displayed);
expect((await inviteDialog.getPinNumber()).length > 0).toBe(displayed);
}
export async function verifyMoreNumbersPage(p: Participant) {

View File

@@ -17,8 +17,7 @@ describe('iFrame API for Chat', () => {
p2 = await joinMuc({ name: 'p2', iFrameApi: true });
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'The environment has the iFrame API disabled.';
return;
}

View File

@@ -15,8 +15,7 @@ describe('Participants presence', () => {
const { p1, p2 } = ctx;
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'The environment has the iFrame API disabled.';
return;
}

View File

@@ -2,6 +2,7 @@ import type { Participant } from '../../../helpers/Participant';
import { setTestProperties } from '../../../helpers/TestProperties';
import { config as testsConfig } from '../../../helpers/TestsConfig';
import WebhookProxy from '../../../helpers/WebhookProxy';
import { expectations } from '../../../helpers/expectations';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
import {
assertDialInDisplayed, assertUrlDisplayed,
@@ -24,8 +25,7 @@ describe('Dial-in', () => {
const room = ctx.roomName;
if (!process.env.DIAL_IN_REST_URL) {
console.log('Dial-in test is disabled, set DIAL_IN_REST_URL to enable.');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
return;
}
@@ -34,14 +34,21 @@ describe('Dial-in', () => {
webhooksProxy = ctx.webhooksProxy;
expect(await p1.isInMuc()).toBe(true);
expect(await isDialInEnabled(p1)).toBe(true);
if (expectations.dialIn.enabled !== null) {
expect(await isDialInEnabled(p1)).toBe(expectations.dialIn.enabled);
}
expect(customerId).toBeDefined();
});
it ('Invite UI', async () => {
await assertUrlDisplayed(p1);
await assertDialInDisplayed(p1);
await verifyMoreNumbersPage(p1);
if (expectations.dialIn.enabled !== null) {
await assertDialInDisplayed(p1, expectations.dialIn.enabled);
}
if (expectations.dialIn.enabled === true) {
// TODO: assert the page is NOT shown when the expectation is false.
await verifyMoreNumbersPage(p1);
}
});
it('dial-in', async () => {

View File

@@ -20,8 +20,7 @@ describe('Dial-out', () => {
const room = ctx.roomName;
if (!dialOutUrl) {
console.log('Dial-out test is disabled, set DIAL_OUT_URL to enable.');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'DIAL_OUT_URL is not set.';
return;
}

View File

@@ -22,15 +22,13 @@ describe('SIP jibri invite', () => {
if (true) {
// This is temporary until we figure out how to fix it and configure it properly.
console.log('SIP jibri test is disabled.');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'This test is disabled as the code doesn\'t work anymore.';
return;
}
if (!dialOutUrl) {
console.log('SIP jibri test is disabled, set SIP_JIBRI_DIAL_OUT_URL to enable.');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'SIP_JIBRI_DIAL_OUT_URL is not set.';
return;
}

View File

@@ -1,4 +1,5 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { expectations } from '../../helpers/expectations';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
import { TOKEN_AUTH_FAILED_TEST_ID, TOKEN_AUTH_FAILED_TITLE_TEST_ID } from '../../pageobjects/Notifications';
@@ -86,11 +87,15 @@ describe('XMPP login and MUC join test', () => {
console.log('Joining a MUC without a token');
const p = await joinJaasMuc();
expect(Boolean(await p.isInMuc())).toBe(false);
if (expectations.jaas.unauthenticatedJoins) {
expect(Boolean(await p.isInMuc())).toBe(true);
} else {
expect(Boolean(await p.isInMuc())).toBe(false);
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
expect(errorText).toContain('not allowed to join');
expect(errorText).toContain('not allowed to join');
}
});
// it('without sending a conference-request', async () => {

View File

@@ -31,8 +31,7 @@ describe('Recording and Live Streaming', () => {
// TODO: what should we do in this case? Add a config for this?
if (await p.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'The environment has the iFrame API disabled.';
return;
}

View File

@@ -27,7 +27,7 @@ describe('Transcriptions', () => {
if (await p1.execute(() => config.disableIframeAPI || !config.transcription?.enabled)) {
// skip the test if iframeAPI or transcriptions are disabled
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'The environment has the iFrame API or transcriptions disabled.';
return;
}

View File

@@ -28,8 +28,7 @@ describe('Visitors', () => {
await moderator.driver.waitUntil(() => moderator.execute(() => APP.conference._room.isVisitorsSupported()), {
timeout: 2000
}).catch(e => {
console.log(`Skipping test due to error: ${e}`);
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = `Because isVisitorsSupported() returned an error: ${e}.`;
});
visitor = await joinJaasMuc({

View File

@@ -0,0 +1,75 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { joinMuc } from '../../helpers/joinMuc';
setTestProperties(__filename, {
usesBrowsers: [ 'p1' ]
});
describe('URL Normalisation', () => {
const tests = [
{
hint: '@ sign and .',
// Room as entered in the URL
room: '@example.com',
// Room as normalized in the URL
roomUrl: 'example.com',
// The room part of the MUC JID
roomJid: 'example.com',
// Tenant as entered in the URL
tenant: 'tenant@example.com',
// Tenant as normalized in the URL
tenantUrl: 'tenantexample.com',
// The tenant part of the MUC JID
tenantJid: 'tenantexample_com'
},
{
hint: 'Dashes',
room: 'foo-bar',
roomUrl: 'foo-bar',
roomJid: 'foo-bar',
tenant: 'tenant-example.com',
tenantUrl: 'tenant-example.com',
tenantJid: 'tenant-example_com'
},
{
hint: 'Cyrillic',
room: 'фоо-бар',
roomUrl: '%D1%84%D0%BE%D0%BE-%D0%B1%D0%B0%D1%80',
roomJid: '%d1%84%d0%be%d0%be-%d0%b1%d0%b0%d1%80',
tenant: 'обитател',
tenantUrl: '%D0%BE%D0%B1%D0%B8%D1%82%D0%B0%D1%82%D0%B5%D0%BB',
tenantJid: '%d0%be%d0%b1%d0%b8%d1%82%d0%b0%d1%82%d0%b5%d0%bb',
}
];
for (const test of tests) {
it(test.hint, async () => {
const fullRoom = `${test.room}${ctx.roomName}`;
const fullRoomUrl = `${test.roomUrl}${ctx.roomName}`;
const p = await joinMuc({
name: 'p1',
token: testsConfig.jwt.preconfiguredToken,
}, {
tenant: test.tenant,
roomName: fullRoom
});
const currentUrlStr = await p.driver.getUrl();
const currentUrl = new URL(currentUrlStr);
const path = currentUrl.pathname;
const parts = path.split('/');
expect(parts[1]).toBe(test.tenantUrl);
expect(parts[2]).toBe(fullRoomUrl);
const mucJid = (await p.execute(() => APP.conference._room.room.roomjid)).split('@');
const roomJid = mucJid[0];
const domain = mucJid[1];
expect(roomJid).toBe(`${test.roomJid}${ctx.roomName}`);
expect(domain.startsWith(`conference.${test.tenantJid}.`)).toBe(true);
});
}
});

View File

@@ -0,0 +1,55 @@
import { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { expectations } from '../../helpers/expectations';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
description: 'This test checks the ability of a moderator to grant the "moderator" role to another participant. The\
test is skipped when the "allModerators" expectation is set.',
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Grant moderator', () => {
let p1: Participant, p2: Participant;
it('setup', async () => {
if (expectations.moderation.allModerators) {
ctx.skipSuiteTests = 'allModerators is expected';
return;
}
await ensureOneParticipant();
p1 = ctx.p1;
expect(await p1.isModerator()).toBe(true);
const functionAvailable = await p1.execute(() => typeof APP.conference._room.grantOwner === 'function');
if (expectations.moderation.grantModerator) {
expect(functionAvailable).toBe(true);
} else {
if (!functionAvailable) {
ctx.skipSuiteTests = 'grantModerator is not available and not expected';
return;
}
}
await ensureTwoParticipants();
p2 = ctx.p2;
expect(await p2.isModerator()).toBe(false);
});
it('grant moderator', async () => {
await p1.getFilmstrip().grantModerator(p2);
await p2.driver.waitUntil(
() => p2.isModerator(),
{
timeout: 3000,
timeoutMsg: 'p2 did not become moderator'
}
);
});
});

View File

@@ -0,0 +1,75 @@
import { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { expectations } from '../../helpers/expectations';
import { ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Kick', () => {
let p1: Participant, p2: Participant;
it('setup', async () => {
await ensureTwoParticipants();
p1 = ctx.p1;
p2 = ctx.p2;
// We verify elsewhere (moderation.spec.ts) that the first participant is a moderator.
if (!await p1.isModerator()) {
ctx.skipSuiteTests = 'the first participant is not a moderator';
}
});
it('kick (p2p disabled)', () => kickAndCheck(p1, p2));
it('setup (p2p enabled)', async () => {
await p1.hangup();
await p2.hangup();
await ensureTwoParticipants({
configOverwrite: {
p2p: {
enabled: true
}
}
});
p1 = ctx.p1;
p2 = ctx.p2;
});
it('kick (p2p enabled)', async () => {
await kickAndCheck(p1, p2);
});
it('non-moderator cannot kick', async () => {
if (!expectations.moderation.allModerators) {
await ensureTwoParticipants();
p2 = ctx.p2;
expect(await p2.isModerator()).toBe(false);
await p2.execute(
epId => APP.conference._room.kickParticipant(epId, 'for funzies'),
await p1.getEndpointId()
);
await p1.driver.pause(3000);
expect(await p1.isInMuc()).toBe(true);
}
});
});
/**
* Kicks the second participant and checks that the participant is removed from the conference and that dialogue is open.
*/
async function kickAndCheck(kicker: Participant, kickee: Participant) {
await kicker.getFilmstrip().kickParticipant(await kickee.getEndpointId());
await kicker.waitForParticipants(0);
// check that the kicked participant sees the kick reason dialog
await kickee.driver.waitUntil(
async () => kickee.isLeaveReasonDialogOpen(), {
timeout: 2000,
timeoutMsg: 'No leave reason dialog shown for p2'
});
}

View File

@@ -0,0 +1,42 @@
import { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { expectations } from '../../helpers/expectations';
import { joinMuc } from '../../helpers/joinMuc';
setTestProperties(__filename, {
description: 'This test asserts that participants have the expected role ("moderator" or not). Failures here\
most likely indicate that the environment or test framework is misconfigured.',
usesBrowsers: [ 'p1', 'p2' ]
});
// Just make sure that users are given moderator rights as specified in the expectations config.
describe('Moderation', () => {
let p1: Participant, p2: Participant;
it('setup', async () => {
p1 = await joinMuc({ name: 'p1', token: testsConfig.jwt.preconfiguredToken });
p2 = await joinMuc({ name: 'p2', token: testsConfig.jwt.preconfiguredToken });
});
it('first moderator', async () => {
if (expectations.moderation.firstModerator) {
expect(await p1.isModerator()).toBe(true);
} else {
expect(await p1.isModerator()).toBe(false);
}
});
it('all moderators', async () => {
if (expectations.moderation.allModerators) {
expect(await p1.isModerator()).toBe(true);
expect(await p2.isModerator()).toBe(true);
}
});
it('auto moderator promotion', async () => {
if (expectations.moderation.autoModerator && !expectations.moderation.allModerators) {
expect(await p1.isModerator()).toBe(true);
expect(await p2.isModerator()).toBe(false);
await p1.hangup();
await p2.driver.waitUntil(async () => (await p2.isModerator()));
}
});
});

View File

@@ -212,8 +212,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
globalAny.ctx.testProperties = testProperties;
if (testProperties.useJaas && !testsConfig.jaas.enabled) {
console.warn(`JaaS is not configured, skipping ${testName}.`);
globalAny.ctx.skipSuiteTests = true;
globalAny.ctx.skipSuiteTests = 'JaaS is not configured';
return;
}
@@ -250,7 +249,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
}
if (!tenant) {
console.log(`Can not configure WebhookProxy, missing tenant in config. Skipping ${testName}.`);
globalAny.ctx.skipSuiteTests = true;
globalAny.ctx.skipSuiteTests = 'WebHookProxy is required but not configured (missing tenant)';
return;
}
@@ -264,7 +263,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
if (testProperties.useWebhookProxy && !globalAny.ctx.webhooksProxy) {
console.warn(`WebhookProxy is not available, skipping ${testName}`);
globalAny.ctx.skipSuiteTests = true;
globalAny.ctx.skipSuiteTests = 'WebhooksProxy is not required but not available';
}
},
@@ -313,14 +312,30 @@ export const config: WebdriverIO.MultiremoteConfig = {
*/
beforeTest(test, context) {
// Use the directory under 'tests/specs' as the parent suite
const match = test.file.match(/.*\/tests\/specs\/([^\/]+)\//);
const dir = match ? match[1] : false;
const dirMatch = test.file.match(/.*\/tests\/specs\/([^\/]+)\//);
const dir = dirMatch ? dirMatch[1] : false;
const fileMatch = test.file.match(/.*\/tests\/specs\/(.*)/);
const file = fileMatch ? fileMatch[1] : false;
if (ctx.testProperties.description) {
AllureReporter.addDescription(ctx.testProperties.description, 'text');
}
if (file) {
AllureReporter.addLink(`https://github.com/jitsi/jitsi-meet/blob/master/tests/specs/${file}`, 'Code');
}
if (dir) {
AllureReporter.addParentSuite(dir);
}
if (ctx.skipSuiteTests) {
if ((typeof ctx.skipSuiteTests) === 'string') {
AllureReporter.addDescription((ctx.testProperties.description || '')
+ '\n\nSkipped because: ' + ctx.skipSuiteTests, 'text');
}
console.log(`Skipping because: ${ctx.skipSuiteTests}`);
context.skip();
return;