Compare commits

..

1 Commits

Author SHA1 Message Date
damencho
5ff3219935 debug: Drop. 2025-10-02 14:40:39 -05:00
79 changed files with 635 additions and 883 deletions

View File

@@ -73,6 +73,7 @@ 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,6 +96,9 @@ 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,6 +101,7 @@ 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,6 +24,8 @@ 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": "Envoyer un message",
"messagebox": "Saisissez un message",
"newMessages": "Nouveaux messages",
"nickname": {
"popover": "Choisissez un pseudonyme",

View File

@@ -570,12 +570,10 @@
"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,11 +158,10 @@ 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/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -87,6 +87,7 @@
"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",
@@ -18259,8 +18260,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"integrity": "sha512-PCMJIfFWIZtDC6UA/53mT79hiqTGNCRE04/XFgWEr7KRf2QIni2tFh3hW1IPW0OjbtMAkJ1KGQpca/3l6sa5Mw==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.4.6",
@@ -21869,6 +21870,14 @@
"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",
@@ -39706,8 +39715,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"integrity": "sha512-PCMJIfFWIZtDC6UA/53mT79hiqTGNCRE04/XFgWEr7KRf2QIni2tFh3hW1IPW0OjbtMAkJ1KGQpca/3l6sa5Mw==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"requires": {
"@jitsi/js-utils": "2.4.6",
"@jitsi/logger": "2.1.1",
@@ -42356,6 +42365,10 @@
"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/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -93,6 +93,7 @@
"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
ditto "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}/sounds"
cp -R "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}"
',
}
end

View File

@@ -73,6 +73,7 @@
"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,6 +4,7 @@ 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,6 +2,7 @@ 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,6 +52,7 @@ 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';
@@ -131,6 +132,7 @@ 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,8 +7,6 @@ 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';
@@ -24,7 +22,7 @@ import {
safeDecodeURIComponent
} from '../util/uri';
import { conferenceWillInit, setObfuscatedRoom } from './actions';
import { setObfuscatedRoom } from './actions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
@@ -620,34 +618,3 @@ 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,10 +23,6 @@ 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,11 +127,6 @@ 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, shouldLeave = true) {
export function disconnect(isRedirect?: boolean) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<void> => {
const state = getState();
@@ -407,26 +407,20 @@ export function disconnect(isRedirect?: boolean, shouldLeave = true) {
// intention to leave the conference.
dispatch(conferenceWillLeave(conference_, isRedirect));
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);
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,6 +78,12 @@ 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,16 +71,11 @@ const useStyles = makeStyles()(theme => {
badge: {
...theme.typography.labelBold,
alignItems: 'center',
backgroundColor: theme.palette.warning01,
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)}`
padding: `0 ${theme.spacing(1)}`,
borderRadius: '100%',
backgroundColor: theme.palette.warning01,
marginLeft: theme.spacing(2)
},
icon: {

View File

@@ -7,9 +7,7 @@ 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';
@@ -19,21 +17,6 @@ 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).
*/
@@ -113,9 +96,7 @@ class Chat extends Component<IProps> {
* @private
* @returns {{
* _messages: Array<Object>,
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _nbUnreadFiles: number
* _nbUnreadMessages: number
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
@@ -123,16 +104,13 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
return {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: getUnreadPollCount(state),
_nbUnreadFiles: getUnreadFilesCount(state)
_nbUnreadMessages: nbUnreadMessages
};
}
export default translate(connect(_mapStateToProps)((props: IProps) => {
const { _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles, dispatch, navigation, t } = props;
const totalUnread = _nbUnreadMessages + _nbUnreadPolls + _nbUnreadFiles;
const unreadMessagesNr = totalUnread > 0;
const { _nbUnreadMessages, dispatch, navigation, t } = props;
const unreadMessagesNr = _nbUnreadMessages > 0;
const isFocused = useIsFocused();
@@ -143,14 +121,14 @@ export default translate(connect(_mapStateToProps)((props: IProps) => {
activeUnreadNr = { unreadMessagesNr }
isFocused = { isFocused }
label = { t('chat.tabs.chat') }
nbUnread = { totalUnread } />
nbUnread = { _nbUnreadMessages } />
)
});
return () => {
isFocused && dispatch(closeChat());
};
}, [ isFocused, _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles ]);
}, [ isFocused, _nbUnreadMessages ]);
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, getUnreadFilesCount } from '../../functions';
import { getUnreadCount } from '../../functions';
interface IProps extends AbstractButtonProps {
@@ -70,7 +70,9 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
return {
_isPollsDisabled: arePollsDisabled(state),
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state) || getUnreadFilesCount(state),
// The toggled icon should also be available for new polls
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state),
visible
};
}

View File

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

View File

@@ -131,16 +131,6 @@ 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,7 +1,6 @@
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 {
@@ -31,7 +30,6 @@ const DEFAULT_STATE = {
notifyPrivateRecipientsChangedTimestamp: undefined,
reactions: {},
nbUnreadMessages: 0,
nbUnreadFiles: 0,
privateMessageRecipient: undefined,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false,
@@ -55,7 +53,6 @@ export interface IChatState {
name: string;
} | ILocalParticipant;
messages: IMessage[];
nbUnreadFiles: number;
nbUnreadMessages: number;
notifyPrivateRecipientsChangedTimestamp?: number;
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
@@ -238,8 +235,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
return {
...state,
focusedTab: action.tabId,
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages,
nbUnreadFiles: action.tabId === ChatTabs.FILE_SHARING ? 0 : state.nbUnreadFiles
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages
};
case SET_CHAT_WIDTH: {
@@ -275,23 +271,6 @@ 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,7 +3,9 @@ import React, { useCallback } from 'react';
import {
BackHandler,
NativeModules,
Platform,
SafeAreaView,
StatusBar,
View,
ViewStyle
} from 'react-native';
@@ -14,6 +16,8 @@ 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';
@@ -92,6 +96,11 @@ 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.
*/
@@ -268,6 +277,7 @@ class Conference extends AbstractConference<IProps, State> {
override render() {
const {
_brandingStyles,
_fullscreenEnabled
} = this.props;
return (
@@ -277,6 +287,13 @@ class Conference extends AbstractConference<IProps, State> {
_brandingStyles
] }>
<BrandingImageBackground />
{
Platform.OS === 'android'
&& <StatusBar
barStyle = 'light-content'
hidden = { _fullscreenEnabled }
translucent = { _fullscreenEnabled } />
}
{ this._renderContent() }
</Container>
);
@@ -573,6 +590,7 @@ 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,14 +39,12 @@ 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, shouldIncrementUnread = false) {
export function addFile(file: IFileMetadata) {
return {
type: ADD_FILE,
file,
shouldIncrementUnread
file
};
}

View File

@@ -6,10 +6,8 @@ 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';
@@ -25,40 +23,12 @@ import { downloadFile } from './utils';
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch, getState }, previousConference) => {
(conference, { dispatch }, previousConference) => {
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_ADDED, (file: IFileMetadata) => {
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));
}
dispatch(addFile(file));
});
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
@@ -66,13 +36,9 @@ StateListenerRegistry.register(
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
dispatch({
type: _FILE_LIST_RECEIVED,
files,
localParticipantId: localParticipant?.id
files
});
});
}
@@ -86,17 +52,6 @@ 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,11 +1,6 @@
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 {
@@ -25,7 +20,6 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.set(action.file.fileId, action.file);
return {
...state,
files: newFiles
};
}
@@ -36,7 +30,6 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.delete(action.fileId);
return {
...state,
files: newFiles
};
}
@@ -50,14 +43,12 @@ 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,6 +11,7 @@ import {
getVirtualScreenshareParticipantByOwnerId
} from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { getAutoPinSetting } from '../video-layout/functions';
import {
@@ -18,7 +19,6 @@ 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,8 +34,12 @@ export function selectParticipantInLargeVideo(participant?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
// Skip large video updates when the large video container is hidden.
if (shouldHideLargeVideo(state)) {
if (isStageFilmstripAvailable(state, 2)) {
return;
}
// Keep Etherpad open.
if (state['features/etherpad'].editing) {
return;
}

View File

@@ -1,7 +1,5 @@
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.
@@ -14,17 +12,3 @@ 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

@@ -1,26 +0,0 @@
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

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

View File

@@ -4,7 +4,6 @@ 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

@@ -0,0 +1,12 @@
/**
* 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

@@ -0,0 +1,21 @@
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

@@ -0,0 +1,22 @@
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

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

View File

@@ -0,0 +1,102 @@
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

@@ -0,0 +1,21 @@
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,7 +13,6 @@ 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 {
@@ -445,11 +444,10 @@ 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 || _iAmVisitor) {
if (iAmRecorder) {
return false;
}

View File

@@ -6,7 +6,6 @@ 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;
@@ -18,8 +17,6 @@ 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", "");
@@ -50,7 +47,6 @@ 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
@@ -82,13 +78,8 @@ function handle_terminate_meeting (event)
module:log("warn", "Room not found")
return { status_code = 404 };
else
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
module:log("info", "Destroy room jid %s", room.jid)
room:destroy(nil, "The meeting has been terminated")
end
event_count_success()
return { status_code = 200 };

View File

@@ -48,7 +48,6 @@ 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;
@@ -106,7 +105,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_jid, actor, jid, display_name, granted)
function notify_lobby_access(room, actor, jid, display_name, granted)
local notify_json = {
value = jid,
name = display_name
@@ -117,12 +116,6 @@ function notify_lobby_access(room_jid, 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
@@ -234,7 +227,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_jid = room.jid;
new_room.main_room = room;
room._data.lobbyroom = new_room.jid;
room:save(true);
return true
@@ -252,8 +245,6 @@ 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;
@@ -421,18 +412,13 @@ 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_jid then
if not room.main_room then
module:log('error', 'No main room(%s) for %s!', room.jid, jid);
return 'none';
end
-- moderators in main room are moderators here
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);
local role = room.main_room.get_affiliation(room.main_room, jid);
if role then
return role;
end
@@ -447,7 +433,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_jid, actor, occupant.nick, display_name, false);
notify_lobby_access(room.main_room, actor, occupant.nick, display_name, false);
end
end);
end
@@ -603,7 +589,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.jid, from, occupant.nick, display_name, true);
notify_lobby_access(room, from, occupant.nick, display_name, true);
end
end
end

View File

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

View File

@@ -11,7 +11,6 @@
module:depends('room_destroy');
local util = module:require "util";
local get_room_from_jid = util.get_room_from_jid;
local is_healthcheck_room = util.is_healthcheck_room;
local main_muc_component_host = module:get_option_string('main_muc');
local lobby_muc_component_host = module:get_option_string('lobby_muc');
@@ -145,13 +144,9 @@ run_when_component_loaded(lobby_muc_component_host, function(host_module, host_n
lobby_module:hook("muc-occupant-left", function(event)
-- Check if room should be destroyed when someone leaves the lobby
local lobby_room = event.room;
if not lobby_room.main_room_jid then
return;
end
local main_room = get_room_from_jid(lobby_room.main_room_jid);
local main_room = lobby_room.main_room;
if not main_room or is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
return;

View File

@@ -54,18 +54,13 @@ function generateToken(session, audience, room, occupant)
local exp = t + options.ttl_seconds;
local presence = occupant:get_presence(session.full_jid);
local _, _, id = extract_subdomain(jid.node(room.jid));
local sub = module.host;
if session.jitsi_web_query_prefix and session.jitsi_web_query_prefix ~= '' then
sub = session.jitsi_web_query_prefix;
end
local payload = {
iss = options.issuer,
aud = audience,
nbf = t,
exp = exp,
sub = sub,
sub = session.jitsi_web_query_prefix or module.host,
context = {
group = session.jitsi_meet_context_group or session.granted_jitsi_meet_context_group_id,
user = session.jitsi_meet_context_user or {

View File

@@ -2,10 +2,6 @@
* 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,7 +4,18 @@
export const config = {
/** Enable debug logging. Note this includes private information from .env */
debug: Boolean(process.env.JITSI_DEBUG?.trim()),
expectationsFile: process.env.EXPECTATIONS?.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-')
);
})(),
jaas: {
customerId: (() => {
if (typeof process.env.JAAS_TENANT !== 'undefined') {

View File

@@ -1,50 +0,0 @@
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,9 +18,8 @@ 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 | string;
skipSuiteTests: boolean;
/**
* Test properties provided by the test file via TestProperties.setTestProperties. Used by the framework to
* set up the context appropriately.

View File

@@ -136,20 +136,6 @@ 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,6 +1,5 @@
import process from 'node:process';
import { expectations } from '../../helpers/expectations';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
@@ -10,22 +9,16 @@ describe('Fake Dial-In', () => {
// check rest url is not configured
if (process.env.DIAL_IN_REST_URL) {
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set';
ctx.skipSuiteTests = true;
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 (configEnabled) {
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
if (await isDialInEnabled(ctx.p1)) {
ctx.skipSuiteTests = true;
}
});

View File

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,41 @@
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.getFilmstrip().isDominantSpeaker(speakerEndpoint),
async () => await otherParticipant1.getLargeVideo().getResource() === speakerEndpoint,
{
timeout: 30_000, // 30 seconds
timeoutMsg: `${activeSpeaker.name} is not selected as active speaker.`
timeoutMsg: 'Active speaker not displayed on large video.'
});
// just a debug print to go in logs

View File

@@ -1,5 +1,5 @@
import { Participant } from '../../helpers/Participant';
import { expectations } from '../../helpers/expectations';
import { config } from '../../helpers/TestsConfig';
import {
ensureOneParticipant,
ensureThreeParticipants, ensureTwoParticipants,
@@ -17,8 +17,7 @@ describe('AVModeration', () => {
if (!await p1.isModerator()
|| (await p1.isModerator() && await p2.isModerator() && await p3.isModerator())) {
ctx.skipSuiteTests = `Unsupported moderator configuration: p1=${await p1.isModerator()},\
p2=${await p2.isModerator()}, p3=${await p3.isModerator()}`;
ctx.skipSuiteTests = true;
}
});
@@ -80,7 +79,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 (!expectations.autoModerator) {
if (!config.autoModerator) {
return;
}

View File

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

View File

@@ -42,7 +42,6 @@ describe('Codec selection', () => {
it('asymmetric codecs with AV1', async () => {
await ensureThreeParticipants({
configOverwrite: {
disableTileView: true,
videoQuality: {
codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
}
@@ -96,7 +95,6 @@ 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 { expectations } from '../../helpers/expectations';
import { config } from '../../helpers/TestsConfig';
import {
ensureOneParticipant,
ensureThreeParticipants,
@@ -14,7 +14,7 @@ describe('Lobby', () => {
await ensureOneParticipant();
if (!await ctx.p1.execute(() => APP.conference._room.isLobbySupported())) {
ctx.skipSuiteTests = 'The environment does not support lobby.';
ctx.skipSuiteTests = true;
}
});
@@ -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 (!expectations.autoModerator) {
if (!config.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 (!expectations.autoModerator) {
if (!config.autoModerator) {
return;
}
const { p1, p2, p3 } = ctx;

View File

@@ -1,12 +1,10 @@
import {
checkForScreensharingTile,
ensureOneParticipant,
ensureTwoParticipants,
hangupAllParticipants,
joinSecondParticipant,
joinThirdParticipant
} from '../../helpers/participants';
import { unmuteVideoAndCheck } from '../helpers/mute';
describe('StartMuted', () => {
it('checkboxes test', async () => {
@@ -139,142 +137,139 @@ 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 */);
});
it('startWithVideoMuted=true can unmute', async () => {
// Maybe disable if there is FF or Safari participant.
await hangupAllParticipants();
// Explicitly enable P2P due to a regression with unmute not updating
// large video while in P2P.
const options = {
configOverwrite: {
p2p: {
enabled: true
},
startWithVideoMuted: true
}
};
await ensureTwoParticipants(options);
const { p1, p2 } = ctx;
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
await Promise.all([
p1.getLargeVideo().waitForSwitchTo(await p2.getEndpointId()),
p2.getLargeVideo().waitForSwitchTo(await p1.getEndpointId())
]);
await unmuteVideoAndCheck(p2, p1);
await p1.getLargeVideo().assertPlaying();
});
it('startWithAudioMuted=true can unmute', async () => {
await hangupAllParticipants();
const options = {
configOverwrite: {
startWithAudioMuted: true,
testing: {
testMode: true,
debugAudioLevels: true
}
}
};
await ensureTwoParticipants(options);
const { p1, p2 } = ctx;
await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, true) ]);
await p1.getToolbar().clickAudioUnmuteButton();
await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, false) ]);
});
it('startWithAudioVideoMuted=true can unmute', async () => {
await hangupAllParticipants();
const options = {
configOverwrite: {
startWithAudioMuted: true,
startWithVideoMuted: true,
p2p: {
enabled: true
}
}
};
await ensureOneParticipant(options);
await joinSecondParticipant({
configOverwrite: {
testing: {
testMode: true,
debugAudioLevels: true
},
p2p: {
enabled: true
}
}
});
const { p1, p2 } = ctx;
await p2.waitForIceConnected();
await p2.waitForSendMedia();
await p2.waitForAudioMuted(p1, true);
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
// Unmute p1's both audio and video and check on p2.
await p1.getToolbar().clickAudioUnmuteButton();
await p2.waitForAudioMuted(p1, false);
await unmuteVideoAndCheck(p1, p2);
await p2.getLargeVideo().assertPlaying();
});
it('test p2p JVB switch and switch back', async () => {
const { p1, p2 } = ctx;
// Mute p2's video just before p3 joins.
await p2.getToolbar().clickVideoMuteButton();
await joinThirdParticipant({
configOverwrite: {
p2p: {
enabled: true
}
}
});
const { p3 } = ctx;
// Unmute p2 and check if its video is being received by p1 and p3.
await unmuteVideoAndCheck(p2, p3);
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
// Mute p2's video just before p3 leaves.
await p2.getToolbar().clickVideoMuteButton();
await p3.hangup();
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
await p2.getToolbar().clickVideoUnmuteButton();
// Check if p2's video is playing on p1.
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
await p1.getLargeVideo().assertPlaying();
});
// it('startWithVideoMuted=true can unmute', async () => {
// // Maybe disable if there is FF or Safari participant.
//
// await hangupAllParticipants();
//
// // Explicitly enable P2P due to a regression with unmute not updating
// // large video while in P2P.
// const options = {
// configOverwrite: {
// p2p: {
// enabled: true
// },
// startWithVideoMuted: true
// }
// };
//
// await ensureTwoParticipants(options);
//
// const { p1, p2 } = ctx;
//
// await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
// await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
//
// await Promise.all([
// p1.getLargeVideo().waitForSwitchTo(await p2.getEndpointId()),
// p2.getLargeVideo().waitForSwitchTo(await p1.getEndpointId())
// ]);
//
// await unmuteVideoAndCheck(p2, p1);
// await p1.getLargeVideo().assertPlaying();
// });
//
// it('startWithAudioMuted=true can unmute', async () => {
// await hangupAllParticipants();
//
// const options = {
// configOverwrite: {
// startWithAudioMuted: true,
// testing: {
// testMode: true,
// debugAudioLevels: true
// }
// }
// };
//
// await ensureTwoParticipants(options);
//
// const { p1, p2 } = ctx;
//
// await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, true) ]);
// await p1.getToolbar().clickAudioUnmuteButton();
// await Promise.all([ p1.waitForAudioMuted(p2, true), p2.waitForAudioMuted(p1, false) ]);
// });
//
// it('startWithAudioVideoMuted=true can unmute', async () => {
// await hangupAllParticipants();
//
// const options = {
// configOverwrite: {
// startWithAudioMuted: true,
// startWithVideoMuted: true,
// p2p: {
// enabled: true
// }
// }
// };
//
// await ensureOneParticipant(options);
// await joinSecondParticipant({
// configOverwrite: {
// testing: {
// testMode: true,
// debugAudioLevels: true
// },
// p2p: {
// enabled: true
// }
// }
// });
//
// const { p1, p2 } = ctx;
//
// await p2.waitForIceConnected();
// await p2.waitForSendMedia();
//
// await p2.waitForAudioMuted(p1, true);
// await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
//
// // Unmute p1's both audio and video and check on p2.
// await p1.getToolbar().clickAudioUnmuteButton();
// await p2.waitForAudioMuted(p1, false);
//
// await unmuteVideoAndCheck(p1, p2);
// await p2.getLargeVideo().assertPlaying();
// });
//
//
// it('test p2p JVB switch and switch back', async () => {
// const { p1, p2 } = ctx;
//
// // Mute p2's video just before p3 joins.
// await p2.getToolbar().clickVideoMuteButton();
//
// await joinThirdParticipant({
// configOverwrite: {
// p2p: {
// enabled: true
// }
// }
// });
//
// const { p3 } = ctx;
//
// // Unmute p2 and check if its video is being received by p1 and p3.
// await unmuteVideoAndCheck(p2, p3);
// await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
//
// // Mute p2's video just before p3 leaves.
// await p2.getToolbar().clickVideoMuteButton();
//
// await p3.hangup();
//
// await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
//
// await p2.getToolbar().clickVideoUnmuteButton();
//
// // Check if p2's video is playing on p1.
// await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
// await p1.getLargeVideo().assertPlaying();
// });
});

View File

@@ -1,7 +1,6 @@
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';
@@ -9,7 +8,7 @@ describe('Dial-In', () => {
it('join participant', async () => {
// check rest url is configured
if (!process.env.DIAL_IN_REST_URL) {
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
ctx.skipSuiteTests = true;
return;
}
@@ -17,7 +16,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 = 'JaaS is configured.';
ctx.skipSuiteTests = true;
return;
}
@@ -26,14 +25,8 @@ describe('Dial-In', () => {
expect(await ctx.p1.isInMuc()).toBe(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.';
if (!await isDialInEnabled(ctx.p1)) {
ctx.skipSuiteTests = true;
}
});
@@ -44,13 +37,13 @@ describe('Dial-In', () => {
dialInPin = await ctx.p1.getDialInPin();
} catch (e) {
console.error('dial-in.test.no-pin');
ctx.skipSuiteTests = 'No dial-in pin is available.';
ctx.skipSuiteTests = true;
throw e;
}
if (dialInPin.length === 0) {
console.error('dial-in.test.no-pin');
ctx.skipSuiteTests = 'The dial-in pin is empty.';
ctx.skipSuiteTests = true;
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 = 'JaaS is configured.';
ctx.skipSuiteTests = true;
return;
}
@@ -19,30 +19,23 @@ describe('Invite', () => {
await ensureOneParticipant();
p1 = ctx.p1;
dialInEnabled = await isDialInEnabled(p1);
});
// The URL should always be displayed.
it('url displayed', () => assertUrlDisplayed(p1));
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);
if (!dialInEnabled) {
return;
}
await assertDialInDisplayed(p1);
});
it('view more numbers page', async () => {
if (expectations.dialIn.enabled === true) {
// TODO: assert the page is NOT shown when the expectation is false.
await verifyMoreNumbersPage(p1);
if (!dialInEnabled) {
return;
}
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, displayed: boolean = false) {
export async function assertDialInDisplayed(p: Participant) {
const inviteDialog = p.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
expect((await inviteDialog.getDialInNumber()).length > 0).toBe(displayed);
expect((await inviteDialog.getPinNumber()).length > 0).toBe(displayed);
expect((await inviteDialog.getDialInNumber()).length > 0).toBe(true);
expect((await inviteDialog.getPinNumber()).length > 0).toBe(true);
}
export async function verifyMoreNumbersPage(p: Participant) {

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ 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,
@@ -25,7 +24,8 @@ describe('Dial-in', () => {
const room = ctx.roomName;
if (!process.env.DIAL_IN_REST_URL) {
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
console.log('Dial-in test is disabled, set DIAL_IN_REST_URL to enable.');
ctx.skipSuiteTests = true;
return;
}
@@ -34,21 +34,14 @@ describe('Dial-in', () => {
webhooksProxy = ctx.webhooksProxy;
expect(await p1.isInMuc()).toBe(true);
if (expectations.dialIn.enabled !== null) {
expect(await isDialInEnabled(p1)).toBe(expectations.dialIn.enabled);
}
expect(await isDialInEnabled(p1)).toBe(true);
expect(customerId).toBeDefined();
});
it ('Invite UI', async () => {
await assertUrlDisplayed(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);
}
await assertDialInDisplayed(p1);
await verifyMoreNumbersPage(p1);
});
it('dial-in', async () => {

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
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';
@@ -87,15 +86,11 @@ describe('XMPP login and MUC join test', () => {
console.log('Joining a MUC without a token');
const p = await joinJaasMuc();
if (expectations.jaas.unauthenticatedJoins) {
expect(Boolean(await p.isInMuc())).toBe(true);
} else {
expect(Boolean(await p.isInMuc())).toBe(false);
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,7 +31,8 @@ describe('Recording and Live Streaming', () => {
// TODO: what should we do in this case? Add a config for this?
if (await p.execute(() => config.disableIframeAPI)) {
ctx.skipSuiteTests = 'The environment has the iFrame API disabled.';
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
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 = 'The environment has the iFrame API or transcriptions disabled.';
ctx.skipSuiteTests = true;
return;
}

View File

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

View File

@@ -1,75 +0,0 @@
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

@@ -1,55 +0,0 @@
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

@@ -1,75 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -58,7 +58,7 @@ const chromePreferences = {
};
const specs = [
'specs/**/*.spec.ts'
'specs/**/startMuted.spec.ts'
];
/**
@@ -212,7 +212,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
globalAny.ctx.testProperties = testProperties;
if (testProperties.useJaas && !testsConfig.jaas.enabled) {
globalAny.ctx.skipSuiteTests = 'JaaS is not configured';
console.warn(`JaaS is not configured, skipping ${testName}.`);
globalAny.ctx.skipSuiteTests = true;
return;
}
@@ -249,7 +250,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
}
if (!tenant) {
console.log(`Can not configure WebhookProxy, missing tenant in config. Skipping ${testName}.`);
globalAny.ctx.skipSuiteTests = 'WebHookProxy is required but not configured (missing tenant)';
globalAny.ctx.skipSuiteTests = true;
return;
}
@@ -263,7 +264,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
if (testProperties.useWebhookProxy && !globalAny.ctx.webhooksProxy) {
console.warn(`WebhookProxy is not available, skipping ${testName}`);
globalAny.ctx.skipSuiteTests = 'WebhooksProxy is not required but not available';
globalAny.ctx.skipSuiteTests = true;
}
},
@@ -312,30 +313,14 @@ export const config: WebdriverIO.MultiremoteConfig = {
*/
beforeTest(test, context) {
// Use the directory under 'tests/specs' as the parent suite
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');
}
const match = test.file.match(/.*\/tests\/specs\/([^\/]+)\//);
const dir = match ? match[1] : false;
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;