Compare commits

...

20 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
39c1542286 fix(recording) skip consent dialog on Spot TV 2025-04-17 22:39:34 -05:00
Saúl Ibarra Corretgé
bf39fff501 fix(polls) halt processing of malformed polls
We need to return something other than nil in order to halt the
processing of the event.

https://prosody.im/doc/developers/moduleapi#modulehook_event_name_handler_priority
2025-04-17 22:39:19 -05:00
damencho
08d5466e5e fix(prosody): Fixes extracting domain from rooms without a domain. 2025-04-11 10:59:21 -05:00
Calin-Teodor
6966fa9eca fix(base/ui): fallback to og value if not found in allTokens 2025-04-09 10:59:47 +03:00
Calinteodor
1f2d999459 feat(base/ui): update tokens (#15688)
* Make createColorTokens generate values based on jitsiTokens and tokens
2025-04-09 10:59:40 +03:00
Mihaela Dumitru
c84f92075d fix(recording) disable option to dismiss consent dialog (#15876) 2025-04-08 16:56:16 +03:00
Дамян Минков
f506f85864 fix(jwt): Fix tenant matching for features when tenant is missing. 2025-03-28 16:37:12 -05:00
damencho
4bae526610 fix(tests): Tests improvements for validate shards.
fix(tests): Wait for the join button to disappear if it was there.

fix(tests): Make sure we give time dialog to submit password.

Give time for the dialog to disappear before entering the new password when retrying or checking for wrong password.

fix(tests): Adds some debug prints for a case reporting failure to leave.

fix(tests): When checking audio levels make sure testMode and debugAudioLevels are present.
2025-03-21 13:40:13 -05:00
Mihaela Dumitru
c13fdf7913 fix(recordings) skip consent for jibri participant (#15825) 2025-03-21 13:48:52 +02:00
Horatiu Muresan
cdcdd1b1bb feat(groupchat-polls-permissions) Implement groupchat and polls creation for web (#15806) 2025-03-20 10:07:28 -05:00
damencho
9017b24fee fix: Fixes dialout dialog and undefined error. 2025-03-12 11:41:19 -05:00
damencho
88cb4d4e00 fix(av-moderation): Skip hiding AV moderation menu. 2025-03-10 21:57:50 -05:00
Saúl Ibarra Corretgé
a88203cf03 fix(tracks) fix toggleCamera on mobile web browsers
Mobile web browsers may not allow opening multiple cameras at the same
time, this is a limitation coming from the underlying paltform. As such,
avoid createing a new track before we have disposed the previous one.

This does introduce a _slight_ fade to back effect, but the alternative
is an exception, so I'll take it! To make matters worse, this is not
necessarily a problem with all devices. It has been observed in a
Samsung S24, at least.
2025-03-10 20:07:35 -05:00
Дамян Минков
7455fe97fa feat(jwt): Adds an option to match tenant. (#15727)
* feat(jwt): Adds an option to match tenant.

There are cases where if tenant doesn't match features are ignore in the backend via prosody and session.jitsi_meet_tenant_mismatch.

* squash: add a comment explaining the change.

* squash: fix comments.

* squash: fix comments2.
2025-03-07 13:00:05 -06:00
Дамян Минков
0520e01194 fix(tests): Fixes the checks when to use token. (#15706)
* feat(tests): Fixes the checks when to use token.

We have few options:
- iframeAPI tests generating tokens via jwtPrivateKeyPath
- tests that just use provided JWT_ACCESS_TOKEN for the first participant to avoid deployments where initial authentication is required
- tests that does not use iframeAPI, but want to use the jwtPrivateKeyPath for a meeting (invite test as JWT_ACCESS_TOKEN does not satisfy some services)

* squash: Bump hangup wait, redirects may take a little bit more.

* squash: Rename forceGenerateToken to preferGenerateToken and fix logic when private key is missing.
2025-03-05 11:42:16 -06:00
Saúl Ibarra Corretgé
5524745099 fix(rn,recording) fix closing the consent dialog (#15699)
* fix(rn,recording) fix closing the consent dialog

* fix(recording) avoid using private properties
2025-03-05 14:56:56 +02:00
Mihaela Dumitru
1d1c7e2e31 feat(recordings) add consent dialog (#15673) 2025-03-05 14:56:45 +02:00
damencho
c9dd4d0f4d feat(tests): Validate shard tests. 2025-03-04 18:59:55 -06:00
Saúl Ibarra Corretgé
dbc06bd75a fix(breakout-rooms) fix processing commands (#15695)
* fix(breakout-rooms) fix processing commands

* squash: fix wrong var name.

* squash: fix move to breakout room.

It can be from breakout to main or from main to breakout.

---------

Co-authored-by: damencho <damencho@jitsi.org>
2025-03-04 12:30:30 -06:00
Hristo Terezov
7554dcad97 chore(package.json): use LJM from release-8443 branch 2025-02-28 12:05:54 -06:00
64 changed files with 921 additions and 305 deletions

View File

@@ -393,6 +393,9 @@ var config = {
// // showPrejoinWarning: true,
// // If true, the notification for recording start will display a link to download the cloud recording.
// // showRecordingLink: true,
// // If true, mutes audio and video when a recording begins and displays a dialog
// // explaining the effect of unmuting.
// // requireConsent: true,
// },
// recordingService: {
@@ -1372,8 +1375,12 @@ var config = {
The config file should be in JSON.
None of the fields are mandatory and the response must have the shape:
{
// Whether participant can only send group chat message if `send-groupchat` feature is enabled in jwt.
groupChatRequiresPermission: false,
// Whether participant can only create polls if `create-polls` feature is enabled in jwt.
pollCreationRequiresPermission: false,
// The domain url to apply (will replace the domain in the sharing conference link/embed section)
inviteDomain: 'example-company.org,
inviteDomain: 'example-company.org',
// The hex value for the colour used as background
backgroundColor: '#fff',
// The url for the image used as background
@@ -1561,6 +1568,8 @@ var config = {
// You can enable tokenAuthUrlAutoRedirect which will detect that you have logged in successfully before
// and will automatically redirect to the token service to get the token for the meeting.
// tokenAuthUrlAutoRedirect: false
// An option to respect the context.tenant jwt field compared to the current tenant from the url
// tokenRespectTenant: false,
// You can put an array of values to target different entity types in the invite dialog.
// Valid values are "phone", "room", "sip", "user", "videosipgw" and "email"

View File

@@ -263,6 +263,7 @@
"Remove": "Remove",
"Share": "Share",
"Submit": "Submit",
"Understand": "I understand",
"WaitForHostMsg": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait.",
"WaitForHostNoAuthMsg": "The conference has not yet started because no moderators have yet arrived. Please wait.",
"WaitingForHostButton": "Wait for moderator",
@@ -393,6 +394,8 @@
"recentlyUsedObjects": "Your recently used objects",
"recording": "Recording",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Not possible while a live stream is active",
"recordingInProgressDescription": "This meeting is being recorded. Your audio and video have been muted. If you choose to unmute, you consent to being recorded.",
"recordingInProgressTitle": "Recording in progress",
"rejoinNow": "Rejoin now",
"remoteControlAllowedMessage": "{{user}} accepted your remote control request!",
"remoteControlDeniedMessage": "{{user}} rejected your remote control request!",
@@ -845,7 +848,7 @@
"actions": {
"admit": "Admit",
"admitAll": "Admit all",
"allow": "Allow attendees to:",
"allow": "Allow non-moderators to:",
"allowVideo": "Allow video",
"askUnmute": "Ask to unmute",
"audioModeration": "Unmute themselves",

9
package-lock.json generated
View File

@@ -62,7 +62,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/v1916.0.0+f5964f9b/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8443",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -16909,8 +16909,7 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1916.0.0+f5964f9b/lib-jitsi-meet.tgz",
"integrity": "sha512-Ywcl3TnZdSIIKcHGQOO9i3xbrVxV27xVLE+Ldngz8MoSAG2b3EmR3UrxRBCNX2M3zUXrwXhY8lMpZeeIl6poUw==",
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#8847230f2dbbb63d17077e5804862ddbc50402df",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -37637,8 +37636,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1916.0.0+f5964f9b/lib-jitsi-meet.tgz",
"integrity": "sha512-Ywcl3TnZdSIIKcHGQOO9i3xbrVxV27xVLE+Ldngz8MoSAG2b3EmR3UrxRBCNX2M3zUXrwXhY8lMpZeeIl6poUw==",
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#8847230f2dbbb63d17077e5804862ddbc50402df",
"from": "lib-jitsi-meet@https://github.com/jitsi/lib-jitsi-meet#release-8443",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",

View File

@@ -68,7 +68,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/v1916.0.0+f5964f9b/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8443",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -542,6 +542,7 @@ export interface IConfig {
recordingSharingUrl?: string;
recordings?: {
recordAudioAndVideo?: boolean;
requireConsent?: boolean;
showPrejoinWarning?: boolean;
showRecordingLink?: boolean;
suggestRecording?: boolean;
@@ -602,6 +603,7 @@ export interface IConfig {
tokenAuthUrl?: string;
tokenAuthUrlAutoRedirect?: string;
tokenLogoutUrl?: string;
tokenRespectTenant?: string;
toolbarButtons?: Array<ToolbarButton>;
toolbarConfig?: {
alwaysVisible?: boolean;

View File

@@ -205,7 +205,10 @@ export default [
'remoteVideoMenu',
'roomPasswordNumberOfDigits',
'readOnlyName',
'recordings',
'recordings.recordAudioAndVideo',
'recordings.showPrejoinWarning',
'recordings.showRecordingLink',
'recordings.suggestRecording',
'replaceParticipant',
'resolution',
'screenshotCapture',

View File

@@ -36,6 +36,11 @@ interface IProps extends AbstractProps, WithTranslation {
*/
descriptionKey?: string | { key: string; params: string; };
/**
* Whether the cancel button is hidden.
*/
isCancelHidden?: Boolean;
/**
* Whether or not the nature of the confirm button is destructive.
*/
@@ -100,6 +105,7 @@ class ConfirmDialog extends AbstractDialog<IProps> {
cancelLabel,
children,
confirmLabel,
isCancelHidden,
isConfirmDestructive,
isConfirmHidden,
t,
@@ -121,10 +127,12 @@ class ConfirmDialog extends AbstractDialog<IProps> {
}
{ this._renderDescription() }
{ children }
<Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
{
!isCancelHidden && <Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
}
{
!isConfirmHidden && <Dialog.Button
label = { t(confirmLabel || 'dialog.confirmYes') }

View File

@@ -1,9 +1,12 @@
import { ParticipantFeaturesKey } from '../participants/types';
/**
* The list of supported meeting features to enable/disable through jwt.
*/
export const MEET_FEATURES = {
export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
BRANDING: 'branding',
CALENDAR: 'calendar',
CREATE_POLLS: 'create-polls',
FLIP: 'flip',
INBOUND_CALL: 'inbound-call',
LIVESTREAMING: 'livestreaming',
@@ -13,6 +16,7 @@ export const MEET_FEATURES = {
RECORDING: 'recording',
ROOM: 'room',
SCREEN_SHARING: 'screen-sharing',
SEND_GROUPCHAT: 'send-groupchat',
SIP_INBOUND_CALL: 'sip-inbound-call',
SIP_OUTBOUND_CALL: 'sip-outbound-call',
TRANSCRIPTION: 'transcription'

View File

@@ -4,7 +4,7 @@ import jwtDecode from 'jwt-decode';
import { IReduxState } from '../../app/types';
import { isVpaasMeeting } from '../../jaas/functions';
import { getLocalParticipant } from '../participants/functions';
import { IParticipantFeatures } from '../participants/types';
import { IParticipantFeatures, ParticipantFeaturesKey } from '../participants/types';
import { parseURLParams } from '../util/parseURLParams';
import { JWT_VALIDATION_ERRORS, MEET_FEATURES } from './constants';
@@ -49,7 +49,12 @@ export function getJwtName(state: IReduxState) {
* @param {boolean} ifNotInFeatures - Default value if features prop exists but does not have the {@code feature}.
* @returns {boolean}
*/
export function isJwtFeatureEnabled(state: IReduxState, feature: string, ifNoToken: boolean, ifNotInFeatures: boolean) {
export function isJwtFeatureEnabled(
state: IReduxState,
feature: ParticipantFeaturesKey,
ifNoToken: boolean,
ifNotInFeatures: boolean
) {
const { jwt } = state['features/base/jwt'];
let { features } = getLocalParticipant(state) || {};
@@ -68,7 +73,7 @@ export function isJwtFeatureEnabled(state: IReduxState, feature: string, ifNoTok
}
interface IIsJwtFeatureEnabledStatelessParams {
feature: string;
feature: ParticipantFeaturesKey;
ifNoToken: boolean;
ifNotInFeatures: boolean;
jwt?: string;
@@ -101,11 +106,11 @@ export function isJwtFeatureEnabledStateless({
return ifNoToken;
}
if (typeof features[feature as keyof typeof features] === 'undefined') {
if (typeof features[feature] === 'undefined') {
return ifNotInFeatures;
}
return String(features[feature as keyof typeof features]) === 'true';
return String(features[feature]) === 'true';
}
/**
@@ -202,7 +207,7 @@ export function validateJwt(jwt: string) {
const { features } = context;
const meetFeatures = Object.values(MEET_FEATURES);
Object.keys(features).forEach(feature => {
(Object.keys(features) as ParticipantFeaturesKey[]).forEach(feature => {
if (meetFeatures.includes(feature)) {
const featureValue = features[feature];

View File

@@ -3,12 +3,14 @@ import jwtDecode from 'jwt-decode';
import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { isVpaasMeeting } from '../../jaas/functions';
import { SET_CONFIG } from '../config/actionTypes';
import { SET_LOCATION_URL } from '../connection/actionTypes';
import { participantUpdated } from '../participants/actions';
import { getLocalParticipant } from '../participants/functions';
import { IParticipant } from '../participants/types';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { parseURIString } from '../util/uri';
import { SET_JWT } from './actionTypes';
import { setJWT } from './actions';
@@ -125,6 +127,8 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
const { jwt, type, ...actionPayload } = action;
if (!Object.keys(actionPayload).length) {
const state = store.getState();
if (jwt) {
let jwtPayload;
@@ -150,9 +154,22 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
const newUser = user ? { ...user } : {};
let features = context.features;
const { tokenRespectTenant } = state['features/base/config'];
// eslint-disable-next-line max-depth
if (!isVpaasMeeting(state) && tokenRespectTenant && context.tenant) {
// we skip checking vpaas meetings as there are other backend rules in place
// this way vpaas users can still use this field if needed
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const { tenant = '' } = parseURIString(locationURL.href) || {};
features = context.tenant === tenant || tenant === '' ? features : {};
}
_overwriteLocalParticipant(
store, { ...newUser,
features: context.features });
features });
// eslint-disable-next-line max-depth
if (context.user && context.user.role === 'visitor') {
@@ -172,7 +189,7 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
// On Web it should eventually be restored from storage, but there's
// no such use case yet.
const { user } = store.getState()['features/base/jwt'];
const { user } = state['features/base/jwt'];
user && _undoOverwriteLocalParticipant(store, user);
}

View File

@@ -57,6 +57,7 @@ export interface ILocalParticipant extends IParticipant {
export interface IParticipantFeatures {
'branding'?: boolean | string;
'calendar'?: boolean | string;
'create-polls'?: boolean | string;
'flip'?: boolean | string;
'inbound-call'?: boolean | string;
'livestreaming'?: boolean | string;
@@ -66,6 +67,7 @@ export interface IParticipantFeatures {
'recording'?: boolean | string;
'room'?: boolean | string;
'screen-sharing'?: boolean | string;
'send-groupchat'?: boolean | string;
'sip-inbound-call'?: boolean | string;
'sip-outbound-call'?: boolean | string;
'transcription'?: boolean | string;
@@ -84,3 +86,5 @@ export interface IJitsiParticipant {
getSources: () => Map<string, Map<string, ISourceInfo>>;
isHidden: () => boolean;
}
export type ParticipantFeaturesKey = keyof IParticipantFeatures;

View File

@@ -6,7 +6,6 @@ import { NOTIFICATION_TIMEOUT, NOTIFICATION_TIMEOUT_TYPE } from '../../notificat
import { getCurrentConference } from '../conference/functions';
import { IJitsiConference } from '../conference/reducer';
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
import { createLocalTrack } from '../lib-jitsi-meet/functions.any';
import { setAudioMuted, setScreenshareMuted, setVideoMuted } from '../media/actions';
import {
CAMERA_FACING_MODE,
@@ -17,7 +16,6 @@ import {
VideoType
} from '../media/constants';
import { getLocalParticipant } from '../participants/functions';
import { updateSettings } from '../settings/actions';
import {
SET_NO_SRC_DATA_NOTIFICATION_UID,
@@ -825,28 +823,3 @@ export function setNoSrcDataNotificationUid(uid?: string) {
uid
};
}
/**
* Toggles the facingMode constraint on the video stream.
*
* @returns {Function}
*/
export function toggleCamera() {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const tracks = state['features/base/tracks'];
const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
const currentFacingMode = localVideoTrack.getCameraFacingMode();
const { localFlipX } = state['features/base/settings'];
const targetFacingMode = currentFacingMode === CAMERA_FACING_MODE.USER
? CAMERA_FACING_MODE.ENVIRONMENT
: CAMERA_FACING_MODE.USER;
// Update the flipX value so the environment facing camera is not flipped, before the new track is created.
dispatch(updateSettings({ localFlipX: targetFacingMode === CAMERA_FACING_MODE.USER ? localFlipX : false }));
const newVideoTrack = await createLocalTrack('video', null, null, { facingMode: targetFacingMode });
dispatch(replaceLocalTrack(localVideoTrack, newVideoTrack));
};
}

View File

@@ -16,15 +16,18 @@ import { getCurrentConference } from '../conference/functions';
import { notifyCameraError, notifyMicError } from '../devices/actions.web';
import { openDialog } from '../dialog/actions';
import { JitsiTrackErrors, JitsiTrackEvents, browser } from '../lib-jitsi-meet';
import { createLocalTrack } from '../lib-jitsi-meet/functions.any';
import { gumPending, setScreenshareMuted } from '../media/actions';
import { MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
import { IGUMPendingState } from '../media/types';
import {
addLocalTrack,
replaceLocalTrack,
toggleCamera
} from './actions.any';
CAMERA_FACING_MODE,
MEDIA_TYPE,
MediaType,
VIDEO_TYPE,
} from '../media/constants';
import { IGUMPendingState } from '../media/types';
import { updateSettings } from '../settings/actions';
import { addLocalTrack, replaceLocalTrack } from './actions.any';
import AllowToggleCameraDialog from './components/web/AllowToggleCameraDialog';
import {
createLocalTracksF,
@@ -509,3 +512,31 @@ export function handleScreenSharingError(
}));
};
}
/**
* Toggles the facingMode constraint on the video stream.
*
* @returns {Function}
*/
export function toggleCamera() {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const tracks = state['features/base/tracks'];
const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
const currentFacingMode = localVideoTrack.getCameraFacingMode();
const { localFlipX } = state['features/base/settings'];
const targetFacingMode = currentFacingMode === CAMERA_FACING_MODE.USER
? CAMERA_FACING_MODE.ENVIRONMENT
: CAMERA_FACING_MODE.USER;
// Update the flipX value so the environment facing camera is not flipped, before the new track is created.
dispatch(updateSettings({ localFlipX: targetFacingMode === CAMERA_FACING_MODE.USER ? localFlipX : false }));
// On mobile only one camera can be open at a time, so first stop the current camera track.
await dispatch(replaceLocalTrack(localVideoTrack, null));
const newVideoTrack = await createLocalTrack('video', null, null, { facingMode: targetFacingMode });
await dispatch(replaceLocalTrack(null, newVideoTrack));
};
}

View File

@@ -1,49 +1,3 @@
// Default color palette
export const colors = {
error03: '#7A141F',
error04: '#A21B29',
error05: '#CB2233',
error06: '#D83848',
error08: '#F24D5F',
primary01: '#00112D',
primary02: '#00225A',
primary03: '#003486',
primary04: '#0045B3',
primary05: '#0056E0',
primary06: '#246FE5',
primary07: '#4687ED',
primary08: '#99BBF3',
primary09: '#CCDDF9',
surface01: '#040404',
surface02: '#141414',
surface03: '#292929',
surface04: '#3D3D3D',
surface05: '#525252',
surface06: '#666',
surface07: '#858585',
surface08: '#A3A3A3',
surface09: '#C2C2C2',
surface10: '#E0E0E0',
surface11: '#FFF',
success04: '#189B55',
success05: '#1EC26A',
warning05: '#F8AE1A',
warning06: '#FFD600',
support01: '#FF9B42',
support02: '#F96E57',
support03: '#DF486F',
support04: '#B23683',
support05: '#73348C',
support06: '#6A50D3',
support07: '#4380E2',
support08: '#00A8B3',
support09: '#2AA076'
};
// Mapping between the token used and the color
export const colorMap = {
@@ -55,108 +9,110 @@ export const colorMap = {
// Container backgrounds
ui01: 'surface02',
ui02: 'surface03',
ui03: 'surface04',
ui03: 'ui02',
ui04: 'surface05',
ui05: 'surface06',
ui06: 'surface07',
ui05: 'ui01',
ui06: 'ui03',
ui07: 'surface08',
ui08: 'surface09',
ui09: 'surface10',
ui10: 'surface11',
ui08: 'ui21',
ui09: 'ui08',
ui10: 'ui04',
// ----- Actions -----
// Primary
action01: 'primary06',
action01Hover: 'primary07',
action01Active: 'primary04',
action01: 'action01',
action01Hover: 'hover01',
action01Active: 'active01',
// Secondary
action02: 'surface10',
action02Hover: 'surface11',
action02Active: 'surface09',
action02: 'action02',
action02Hover: 'hover02',
action02Active: 'active02',
// Destructive
actionDanger: 'error05',
actionDangerHover: 'error06',
actionDangerActive: 'error04',
actionDanger: 'action03',
actionDangerHover: 'hover03',
actionDangerActive: 'active03',
// Tertiary
action03: 'transparent',
action03Hover: 'surface04',
action03Hover: 'hover05',
action03Active: 'surface03',
// Disabled
disabled01: 'surface09',
disabled01: 'disabled01',
// Focus
focus01: 'primary07',
focus01: 'focus01',
// ----- Links -----
link01: 'primary07',
link01Hover: 'primary08',
link01Active: 'primary06',
link01: 'action01',
link01Hover: 'hover07',
link01Active: 'action04',
// ----- Text -----
// Primary
text01: 'surface11',
text01: 'textColor01',
// Secondary
text02: 'surface09',
text02: 'textColor02',
// Tertiary
text03: 'surface07',
text03: 'ui03',
// High-contrast
text04: 'surface01',
// Error
textError: 'error08',
textError: 'alertRed',
// ----- Icons -----
// Primary
icon01: 'surface11',
icon01: 'icon01',
// Secondary
icon02: 'surface09',
icon02: 'ui21',
// Tertiary
icon03: 'surface07',
icon03: 'icon07',
// High-contrast
icon04: 'surface01',
// Error
iconError: 'error06',
iconError: 'action03',
// Normal
iconNormal: 'action04',
// Success
iconSuccess: 'alertGreen',
// Warning
iconWarning: 'warning01',
// ----- Forms -----
field01: 'surface04',
field01: 'ui02',
// ----- Feedback -----
// Success
success01: 'success05',
success02: 'success04',
success02: 'success01',
// Warning
warning01: 'warning05',
warning01: 'warning01',
warning02: 'warning06',
// ----- Support -----
support01: 'support01',
support02: 'support02',
support03: 'support03',
support04: 'support04',
support05: 'support05',
support06: 'support06',
support07: 'support07',
support08: 'support08',
support09: 'support09'
support06: 'support06'
};
@@ -175,19 +131,9 @@ export const spacing
= [ 0, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 ];
export const typography = {
labelRegular: {
fontSize: 12,
lineHeight: 16,
fontWeight: font.weightRegular,
letterSpacing: 0.16
},
labelRegular: 'label01',
labelBold: {
fontSize: 12,
lineHeight: 16,
fontWeight: font.weightSemiBold,
letterSpacing: 0.16
},
labelBold: 'labelBold01',
bodyShortRegularSmall: {
fontSize: 10,
@@ -252,19 +198,9 @@ export const typography = {
letterSpacing: 0
},
heading1: {
fontSize: 54,
lineHeight: 64,
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
heading1: 'heading01',
heading2: {
fontSize: 42,
lineHeight: 50,
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
heading2: 'heading02',
heading3: {
fontSize: 32,

View File

@@ -1,11 +1,10 @@
import { colorMap, colors, font, shape, spacing, typography } from '../Tokens';
import { colorMap, font, shape, spacing, typography } from '../Tokens';
import { createNativeTheme } from '../functions.native';
import updateTheme from './updateTheme.native';
export default createNativeTheme(updateTheme({
font,
colors,
colorMap,
spacing,
shape,

View File

@@ -1,9 +1,8 @@
import { breakpoints, colorMap, colors, font, shape, spacing, typography } from '../Tokens';
import { breakpoints, colorMap, font, shape, spacing, typography } from '../Tokens';
import { createWebTheme } from '../functions.web';
export default createWebTheme({
font,
colors,
colorMap,
spacing,
shape,

View File

@@ -8,10 +8,10 @@ import { createColorTokens } from './utils';
* @param {Object} arg - The ui tokens.
* @returns {Object}
*/
export function createNativeTheme({ font, colors, colorMap, shape, spacing, typography }: any): any {
export function createNativeTheme({ font, colorMap, shape, spacing, typography }: any): any {
return {
...DefaultTheme,
palette: createColorTokens(colorMap, colors),
palette: createColorTokens(colorMap),
shape,
spacing,
typography: {

View File

@@ -16,7 +16,6 @@ declare module '@mui/material/styles' {
interface ThemeProps {
breakpoints: Object;
colorMap: Object;
colors: Object;
font: Object;
shape: Object;
spacing: Array<number>;
@@ -29,10 +28,10 @@ interface ThemeProps {
* @param {Object} arg - The ui tokens.
* @returns {Object}
*/
export function createWebTheme({ font, colors, colorMap, shape, spacing, typography, breakpoints }: ThemeProps) {
export function createWebTheme({ font, colorMap, shape, spacing, typography, breakpoints }: ThemeProps) {
return createTheme(adaptV4Theme({
spacing,
palette: createColorTokens(colorMap, colors),
palette: createColorTokens(colorMap),
shape,
typography: {
// @ts-ignore

View File

@@ -0,0 +1,14 @@
{
"success05": "#1EC26A",
"support05": "#73348C",
"support06": "#6A50D3",
"surface01": "#040404",
"surface02": "#141414",
"surface03": "#292929",
"surface05": "#525252",
"surface08": "#A3A3A3",
"warning06": "#FFD600"
}

View File

@@ -0,0 +1,211 @@
{
"action01": "#4687ED",
"action02": "#E0E0E0",
"action03": "#D83848",
"action04": "#246FE5",
"action05": "#4687ED",
"action06": "#0056E0",
"action07": "#D7E3F9",
"action08": "#99BBF3",
"action09": "#4687ED",
"active01": "#0056E0",
"active02": "#C2C2C2",
"active03": "#CB2233",
"active04": "#189B55",
"active05": "#666666",
"active07": "#0056E0",
"active08": "#525252",
"active09": "#3D3D3D",
"active10": "#3D3D3D",
"active11": "#CCDDF9",
"active12": "#666666",
"alertGreen": "#189B55",
"alertRed": "#F24D5F",
"alertYellow": "#F8AE1A",
"body01": {
"fontFamily": "Inter, sans-serif",
"fontSize": "0.875rem",
"lineHeight": "1.25rem",
"fontWeight": 400,
"letterSpacing": "-0.006rem"
},
"body02": {
"fontFamily": "Inter, sans-serif",
"fontSize": "1rem",
"lineHeight": "1.5rem",
"fontWeight": 400,
"letterSpacing": "-0.011rem"
},
"bodyBold01": {
"fontFamily": "Inter, sans-serif",
"fontSize": "0.875rem",
"lineHeight": "1.25rem",
"fontWeight": 600,
"letterSpacing": "-0.006rem"
},
"bodyBold02": {
"fontFamily": "Inter, sans-serif",
"fontSize": "1rem",
"lineHeight": "1.5rem",
"fontWeight": 600,
"letterSpacing": "-0.011rem"
},
"brand01": "#FFFFFF",
"bulletList01": {
"fontFamily": "Inter, sans-serif",
"fontSize": "0.875rem",
"lineHeight": "1.5rem",
"fontWeight": 400,
"letterSpacing": "-0.006rem"
},
"data01": "#C15C97",
"data02": "#4079D3",
"data03": "#A276B2",
"data04": "#4CAC9C",
"data05": "#E76782",
"data06": "#8CA7D1",
"data07": "#B23683",
"data08": "#FFA95E",
"data09": "#8B559F",
"data10": "#009B89",
"data11": "#858585",
"dataText01": "#292929",
"dataText02": "#292929",
"dataText03": "#292929",
"dataText04": "#000000",
"dataText05": "#000000",
"dataText06": "#000000",
"dataText07": "#FFFFFF",
"dataText08": "#000000",
"dataText09": "#FFFFFF",
"dataText10": "#FFFFFF",
"dataText11": "#000000",
"disabled01": "#C2C2C2",
"disabled02": "#666666",
"disabled03": "#C2C2C2",
"disabled04": "#858585",
"error01": "#F24D5F",
"focus01": "#D7E3F9",
"heading01": {
"fontFamily": "Inter, sans-serif",
"fontSize": "1.25rem",
"lineHeight": "1.75rem",
"fontWeight": 600,
"letterSpacing": "-0.017rem"
},
"heading02": {
"fontFamily": "Inter, sans-serif",
"fontSize": "1.75rem",
"lineHeight": "2.5rem",
"fontWeight": 600,
"letterSpacing": "-0.020rem"
},
"highlight01": "#F8AE1A",
"hover01": "#4687ED",
"hover02": "#F1F1F1",
"hover03": "#F24D5F",
"hover04": "#4BCE88",
"hover05": "#3D3D3D",
"hover06": "#666666",
"hover07": "#99BBF3",
"hover08": "#2F2E32",
"hover09": "#666666",
"hover10": "#292929",
"hover11": "#99BBF3",
"hover12": "#3D3D3D",
"hover13": "#525252",
"icon01": "#FFFFFF",
"icon02": "#FFFFFF",
"icon03": "#666666",
"icon04": "#0056E0",
"icon05": "#292929",
"icon06": "#666666",
"icon07": "#858585",
"icon08": "#292929",
"info01": "#666666",
"label01": {
"fontFamily": "Inter, sans-serif",
"fontSize": "0.75rem",
"lineHeight": "1rem",
"fontWeight": 400,
"letterSpacing": "normal"
},
"labelBold01": {
"fontFamily": "Inter, sans-serif",
"fontSize": "0.75rem",
"lineHeight": "1rem",
"fontWeight": 600,
"letterSpacing": "normal"
},
"overlay01": "#292929BF",
"shadow01": "#141414",
"shadowHigh": "0px 2px 20px 9px #141414",
"shadowLow": "0px 1px 2px 1px #141414",
"shadowMedium": "0px 2px 8px 2px #141414",
"statusAvailable01": "#189B55",
"statusAway01": "#DD7011",
"statusBusy01": "#F24D5F",
"statusOffline01": "#666666",
"statusWrapup01": "#8B559F",
"success01": "#189B55",
"textColor01": "#FFFFFF",
"textColor02": "#C2C2C2",
"textColor04": "#292929",
"textColor05": "#C2C2C2",
"textColor06": "#FFFFFF",
"textColor07": "#292929",
"textColor08": "#FFFFFF",
"textColor09": "#000000",
"textColor10": "#E7E3FF",
"ui01": "#666666",
"ui02": "#3D3D3D",
"ui03": "#858585",
"ui04": "#FFFFFF",
"ui05": "#2F2E32",
"ui06": "#171719",
"ui07": "#F1F1F1",
"ui08": "#E0E0E0",
"ui09": "#CCDDF9",
"ui10": "#666666",
"ui11": "#0C0C0D",
"ui12": "#212124",
"ui13": "#003486",
"ui14": "#127440",
"ui15": "#00225A",
"ui16": "#F5D3D6",
"ui17": "#FEEFD1",
"ui18": "#D2F3E1",
"ui19": "#E0E0E0",
"ui20": "#E8DCEC",
"ui21": "#C2C2C2",
"ui22": "#CCDDF9",
"ui23": "#3C2F8E",
"ui24": "#5F50BE",
"ui25": "#171719",
"warning01": "#F8AE1A",
"warning02": "#F8AE1A",
"warning03": "#9F701C"
}

View File

@@ -1,13 +1,21 @@
import { merge } from 'lodash';
import * as jitsiTokens from './jitsiTokens.json';
import * as tokens from './tokens.json';
/**
* Creates the color tokens based on the color theme and the association map.
* If a key is not found in the association map it defaults to the current value.
*
* @param {Object} colorMap - A map between the token name and the actual color value.
* @param {Object} colors - An object containing all the theme colors.
* @returns {Object}
*/
export function createColorTokens(colorMap: Object, colors: Object): any {
export function createColorTokens(colorMap: Object): any {
const allTokens = merge({}, tokens, jitsiTokens);
return Object.entries(colorMap)
.reduce((result, [ token, value ]: [any, keyof Object]) =>
Object.assign(result, { [token]: colors[value] || value }), {});
.reduce((result, [ token, value ]: [any, string]) => {
const color = allTokens[value as keyof typeof allTokens] || value;
return Object.assign(result, { [token]: color });
}, {});
}

View File

@@ -0,0 +1,9 @@
/**
* Checks if Jitsi Meet is running on Spot TV.
*
* @returns {boolean} Whether or not Jitsi Meet is running on Spot TV.
*/
export function isSpotTV(): boolean {
return navigator.userAgent.includes('SpotElectron/');
}

View File

@@ -8,7 +8,7 @@ import { translate } from '../../../base/i18n/functions';
import { IconFaceSmile, IconSend } from '../../../base/icons/svg';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { areSmileysDisabled } from '../../functions';
import { areSmileysDisabled, isSendGroupChatDisabled } from '../../functions';
import SmileysPanel from './SmileysPanel';
@@ -22,6 +22,8 @@ interface IProps extends WithTranslation {
*/
_areSmileysDisabled: boolean;
_isSendGroupChatDisabled: boolean;
/**
* The id of the message recipient, if any.
*/
@@ -145,7 +147,8 @@ class ChatInput extends Component<IProps, IState> {
value = { this.state.message } />
<Button
accessibilityLabel = { this.props.t('chat.sendButton') }
disabled = { !this.state.message.trim() }
disabled = { !this.state.message.trim()
|| (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) }
icon = { IconSend }
onClick = { this._onSubmitMessage }
size = { isMobileBrowser() ? 'large' : 'medium' } />
@@ -170,10 +173,20 @@ class ChatInput extends Component<IProps, IState> {
* @returns {void}
*/
_onSubmitMessage() {
const {
_isSendGroupChatDisabled,
_privateMessageRecipientId,
onSend
} = this.props;
if (_isSendGroupChatDisabled && !_privateMessageRecipientId) {
return;
}
const trimmed = this.state.message.trim();
if (trimmed) {
this.props.onSend(trimmed);
onSend(trimmed);
this.setState({ message: '' });
@@ -273,10 +286,12 @@ class ChatInput extends Component<IProps, IState> {
*/
const mapStateToProps = (state: IReduxState) => {
const { privateMessageRecipient } = state['features/chat'];
const isGroupChatDisabled = isSendGroupChatDisabled(state);
return {
_areSmileysDisabled: areSmileysDisabled(state),
_privateMessageRecipientId: privateMessageRecipient?.id
_privateMessageRecipientId: privateMessageRecipient?.id,
_isSendGroupChatDisabled: isGroupChatDisabled
};
};

View File

@@ -7,6 +7,7 @@ import emojiAsciiAliases from 'react-emoji-render/data/asciiAliases';
import { IReduxState } from '../app/types';
import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
import i18next from '../base/i18n/i18next';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { getParticipantById } from '../base/participants/functions';
import { escapeRegexp } from '../base/util/helpers';
@@ -190,3 +191,20 @@ export function getPrivateNoticeMessage(message: IMessage) {
recipient: message.messageType === MESSAGE_TYPE_LOCAL ? message.recipient : i18next.t('chat.you')
});
}
/**
* Check if participant is not allowed to send group messages.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - Returns true if the participant is not allowed to send group messages.
*/
export function isSendGroupChatDisabled(state: IReduxState) {
const { groupChatRequiresPermission } = state['features/dynamic-branding'];
if (!groupChatRequiresPermission) {
return false;
}
return !isJwtFeatureEnabled(state, 'send-groupchat', false, false);
}

View File

@@ -42,7 +42,7 @@ import {
SEND_REACTION,
SET_IS_POLL_TAB_FOCUSED
} from './actionTypes';
import { addMessage, addMessageReaction, clearMessages, closeChat } from './actions.any';
import { addMessage, addMessageReaction, clearMessages, closeChat, setPrivateMessageRecipient } from './actions.any';
import { ChatPrivacyDialog } from './components';
import {
INCOMING_MSG_SOUND_ID,
@@ -52,7 +52,7 @@ import {
MESSAGE_TYPE_REMOTE,
MESSAGE_TYPE_SYSTEM
} from './constants';
import { getUnreadCount } from './functions';
import { getUnreadCount, isSendGroupChatDisabled } from './functions';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
/**
@@ -161,13 +161,29 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case OPEN_CHAT:
case OPEN_CHAT: {
unreadCount = 0;
if (typeof APP !== 'undefined') {
APP.API.notifyChatUpdated(unreadCount, true);
}
const { privateMessageRecipient } = store.getState()['features/chat'];
if (
isSendGroupChatDisabled(store.getState())
&& privateMessageRecipient
&& !action.participant
) {
const participant = getParticipantById(store.getState(), privateMessageRecipient.id);
if (participant) {
action.participant = participant;
}
}
break;
}
case SET_IS_POLL_TAB_FOCUSED: {
dispatch(resetNbUnreadPollsMessages());
@@ -307,6 +323,12 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
isGuest,
messageId,
privateMessage: false });
if (isSendGroupChatDisabled(store.getState()) && participantId) {
const participant = getParticipantById(store, participantId);
store.dispatch(setPrivateMessageRecipient(participant));
}
}
);

View File

@@ -1,7 +1,7 @@
import { Theme } from '@mui/material';
import { adaptV4Theme, createTheme } from '@mui/material/styles';
import { breakpoints, colorMap, colors, font, shape, spacing, typography } from '../base/ui/Tokens';
import { breakpoints, colorMap, font, shape, spacing, typography } from '../base/ui/Tokens';
import { createColorTokens } from '../base/ui/utils';
/**
@@ -19,7 +19,7 @@ export function createMuiBrandingTheme(customTheme: Theme) {
spacing: customSpacing
} = customTheme;
const newPalette = createColorTokens(colorMap, colors);
const newPalette = createColorTokens(colorMap);
if (customPalette) {
overwriteRecurrsive(newPalette, customPalette);

View File

@@ -76,6 +76,15 @@ const DEFAULT_STATE = {
*/
didPageUrl: '',
/**
* Whether participant can only send group chat message if `send-groupchat`
* feature is enabled in jwt.
*
* @public
* @type {boolean}
*/
groupChatRequiresPermission: false,
/**
* The custom invite domain.
*
@@ -117,6 +126,14 @@ const DEFAULT_STATE = {
*/
muiBrandedTheme: undefined,
/**
* Whether participant can only create polls if `create-polls` feature is enabled in jwt.
*
* @public
* @type {boolean}
*/
pollCreationRequiresPermission: false,
/**
* The lobby/prejoin background.
*
@@ -152,12 +169,15 @@ export interface IDynamicBrandingState {
defaultBranding: boolean;
defaultTranscriptionLanguage?: boolean;
didPageUrl: string;
groupChatRequiresPermission: boolean;
inviteDomain: string;
labels: Object | null;
logoClickUrl: string;
logoImageUrl: string;
muiBrandedTheme?: boolean;
pollCreationRequiresPermission: boolean;
premeetingBackground: string;
requireRecordingConsent?: boolean;
sharedVideoAllowedURLDomains?: Array<string>;
showGiphyIntegration?: boolean;
supportUrl?: string;
@@ -178,14 +198,17 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
brandedIcons,
defaultBranding,
didPageUrl,
groupChatRequiresPermission,
inviteDomain,
labels,
logoClickUrl,
logoImageUrl,
muiBrandedTheme,
pollCreationRequiresPermission,
premeetingBackground,
sharedVideoAllowedURLDomains,
showGiphyIntegration,
requireRecordingConsent,
supportUrl,
virtualBackgrounds
} = action.value;
@@ -197,14 +220,17 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
brandedIcons,
defaultBranding,
didPageUrl,
groupChatRequiresPermission,
inviteDomain,
labels,
logoClickUrl,
logoImageUrl,
muiBrandedTheme,
pollCreationRequiresPermission,
premeetingBackground,
sharedVideoAllowedURLDomains,
showGiphyIntegration,
requireRecordingConsent,
supportUrl,
customizationFailed: false,
customizationReady: true,

View File

@@ -203,8 +203,6 @@ class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
);
}
_isAddDisabled: () => boolean;
/**
* Callback invoked when a selection has been made but before it has been
* set as selected.

View File

@@ -1,5 +1,6 @@
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { ParticipantFeaturesKey } from '../base/participants/types';
import PremiumFeatureDialog from './components/web/PremiumFeatureDialog';
import { isFeatureDisabled } from './functions';
@@ -7,11 +8,11 @@ import { isFeatureDisabled } from './functions';
/**
* Shows a dialog prompting users to upgrade, if requested feature is disabled.
*
* @param {string} feature - The feature to check availability for.
* @param {ParticipantFeaturesKey} feature - The feature to check availability for.
*
* @returns {Function}
*/
export function maybeShowPremiumFeatureDialog(feature: string) {
export function maybeShowPremiumFeatureDialog(feature: ParticipantFeaturesKey) {
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
if (isFeatureDisabled(getState(), feature)) {
dispatch(openDialog(PremiumFeatureDialog));

View File

@@ -1,5 +1,6 @@
import { IReduxState } from '../app/types';
import { IJitsiConference } from '../base/conference/reducer';
import { ParticipantFeaturesKey } from '../base/participants/types';
import { VPAAS_TENANT_PREFIX } from './constants';
import logger from './logger';
@@ -105,10 +106,10 @@ export async function sendGetDetailsRequest({ appId, baseUrl }: {
* Returns the billing id for vpaas meetings.
*
* @param {IReduxState} state - The state of the app.
* @param {string} feature - Feature to be looked up for disable state.
* @param {ParticipantFeaturesKey} feature - Feature to be looked up for disable state.
* @returns {boolean}
*/
export function isFeatureDisabled(state: IReduxState, feature: string) {
export function isFeatureDisabled(state: IReduxState, feature: ParticipantFeaturesKey) {
return state['features/jaas'].disabledFeatures.includes(feature);
}

View File

@@ -10,6 +10,7 @@ import Watermarks from '../../base/react/components/web/Watermarks';
import { getHideSelfView } from '../../base/settings/functions.any';
import { getVideoTrackByParticipant } from '../../base/tracks/functions.web';
import { setColorAlpha } from '../../base/util/helpers';
import { isSpotTV } from '../../base/util/spot';
import StageParticipantNameLabel from '../../display-name/components/web/StageParticipantNameLabel';
import { FILMSTRIP_BREAKPOINT } from '../../filmstrip/constants';
import { getVerticalViewMaxWidth, isFilmstripResizable } from '../../filmstrip/functions.web';
@@ -23,8 +24,6 @@ import { getLargeVideoParticipant } from '../functions';
import ScreenSharePlaceholder from './ScreenSharePlaceholder.web';
// Hack to detect Spot.
const SPOT_DISPLAY_NAME = 'Meeting Room';
interface IProps {
@@ -352,20 +351,20 @@ function _mapStateToProps(state: IReduxState) {
const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
const { isOpen: isChatOpen } = state['features/chat'];
const { width: verticalFilmstripWidth, visible } = state['features/filmstrip'];
const { defaultLocalDisplayName, hideDominantSpeakerBadge } = state['features/base/config'];
const { hideDominantSpeakerBadge } = state['features/base/config'];
const { seeWhatIsBeingShared } = state['features/large-video'];
const localParticipantId = getLocalParticipant(state)?.id;
const largeVideoParticipant = getLargeVideoParticipant(state);
const videoTrack = getVideoTrackByParticipant(state, largeVideoParticipant);
const isLocalScreenshareOnLargeVideo = largeVideoParticipant?.id?.includes(localParticipantId ?? '')
&& videoTrack?.videoType === VIDEO_TYPE.DESKTOP;
const isOnSpot = defaultLocalDisplayName === SPOT_DISPLAY_NAME;
return {
_backgroundAlpha: state['features/base/config'].backgroundAlpha,
_customBackgroundColor: backgroundColor,
_customBackgroundImageUrl: backgroundImageUrl,
_displayScreenSharingPlaceholder: Boolean(isLocalScreenshareOnLargeVideo && !seeWhatIsBeingShared && !isOnSpot),
_displayScreenSharingPlaceholder:
Boolean(isLocalScreenshareOnLargeVideo && !seeWhatIsBeingShared && !isSpotTV()),
_hideSelfView: getHideSelfView(state),
_isChatOpen: isChatOpen,
_isScreenSharing: Boolean(isLocalScreenshareOnLargeVideo),

View File

@@ -9,7 +9,6 @@ import {
IconUsers,
IconWarning
} from '../../../base/icons/svg';
import { colors } from '../../../base/ui/Tokens';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import Button from '../../../base/ui/components/native/Button';
import IconButton from '../../../base/ui/components/native/IconButton';
@@ -30,10 +29,10 @@ import styles from './styles';
*/
const ICON_COLOR = {
error: colors.error06,
normal: colors.primary06,
success: colors.success05,
warning: colors.warning05
error: BaseTheme.palette.iconError,
normal: BaseTheme.palette.iconNormal,
success: BaseTheme.palette.iconSuccess,
warning: BaseTheme.palette.iconWarning
};

View File

@@ -22,8 +22,7 @@ import Icon from '../../../base/icons/components/Icon';
import { IconCheck, IconRaiseHand, IconVideoOff } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { raiseHand } from '../../../base/participants/actions';
import { getParticipantCount, getRaiseHandsQueue, isEveryoneModerator, isLocalParticipantModerator }
from '../../../base/participants/functions';
import { getRaiseHandsQueue, isLocalParticipantModerator } from '../../../base/participants/functions';
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
import MuteEveryonesVideoDialog
from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
@@ -47,8 +46,6 @@ export const ContextMenuMore = () => {
const { t } = useTranslation();
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
const allModerators = useSelector(isEveryoneModerator);
const participantCount = useSelector(getParticipantCount);
const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
@@ -79,7 +76,7 @@ export const ContextMenuMore = () => {
src = { IconRaiseHand } />
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.lowerAllHands')}</Text>
</TouchableOpacity> }
{isModerationSupported && ((participantCount === 1 || !allModerators)) && <>
{isModerationSupported && <>
{/* @ts-ignore */}
<Divider style = { styles.divider } />
<View style = { styles.contextMenuItem as ViewStyle }>

View File

@@ -21,11 +21,7 @@ import {
IconVideoOff
} from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import {
getParticipantCount,
getRaiseHandsQueue,
isEveryoneModerator
} from '../../../base/participants/functions';
import { getRaiseHandsQueue } from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
@@ -88,9 +84,7 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
const dispatch = useDispatch();
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
const raisedHandsQueue = useSelector(getRaiseHandsQueue);
const allModerators = useSelector(isEveryoneModerator);
const isModeratorSettingsTabEnabled = useSelector(shouldShowModeratorSettings);
const participantCount = useSelector(getParticipantCount);
const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
const isBreakoutRoom = useSelector(isInBreakoutRoom);
@@ -151,7 +145,7 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
text: t('participantsPane.actions.stopEveryonesVideo')
} ] } />
{raisedHandsQueue.length !== 0 && <LowerHandButton />}
{!isBreakoutRoom && isModerationSupported && (participantCount === 1 || !allModerators) && (
{!isBreakoutRoom && isModerationSupported && (
<ContextMenuItemGroup actions = { actions }>
<div className = { classes.text }>
<span>{t('participantsPane.actions.allow')}</span>

View File

@@ -1,5 +1,8 @@
import React, { ComponentType, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { isCreatePollDisabled } from '../functions';
/*
* Props that will be passed by the AbstractPollsPane to its
@@ -7,6 +10,7 @@ import { useTranslation } from 'react-i18next';
**/
export type AbstractProps = {
createMode: boolean;
isCreatePollsDisabled: boolean;
onCreate: () => void;
setCreateMode: (mode: boolean) => void;
t: Function;
@@ -21,6 +25,7 @@ export type AbstractProps = {
*/
const AbstractPollsPane = (Component: ComponentType<AbstractProps>) => () => {
const isCreatePollsDisabled = useSelector(isCreatePollDisabled);
const [ createMode, setCreateMode ] = useState(false);
const onCreate = () => {
@@ -31,6 +36,7 @@ const AbstractPollsPane = (Component: ComponentType<AbstractProps>) => () => {
return (<Component
createMode = { createMode }
isCreatePollsDisabled = { isCreatePollsDisabled }
/* eslint-disable react/jsx-no-bind */
onCreate = { onCreate }
setCreateMode = { setCreateMode }

View File

@@ -28,7 +28,7 @@ const useStyles = makeStyles()(() => {
};
});
const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) => {
const PollsPane = ({ createMode, isCreatePollsDisabled, onCreate, setCreateMode, t }: AbstractProps) => {
const { classes } = useStyles();
return createMode
@@ -40,6 +40,7 @@ const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) =>
<div className = { classes.footer }>
<Button
accessibilityLabel = { t('polls.create.create') }
disabled = { isCreatePollsDisabled }
fullWidth = { true }
labelKey = { 'polls.create.create' }
onClick = { onCreate } />

View File

@@ -1,4 +1,5 @@
import { IReduxState } from '../app/types';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { IAnswerData } from './types';
@@ -62,3 +63,19 @@ export function hasIdenticalAnswers(currentAnswers: Array<IAnswerData>): boolean
return currentAnswersSet.size !== nonEmptyCurrentAnswers.length;
}
/**
* Check if participant is not allowed to create polls.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - Returns true if the participant is not allowed to create polls.
*/
export function isCreatePollDisabled(state: IReduxState) {
const { pollCreationRequiresPermission } = state['features/dynamic-branding'];
if (!pollCreationRequiresPermission) {
return false;
}
return !isJwtFeatureEnabled(state, 'create-polls', false, false);
}

View File

@@ -1 +1,2 @@
export { default as StartRecordingDialog } from './native/StartRecordingDialog';
export { default as RecordingConsentDialog } from './native/RecordingConsentDialog';

View File

@@ -1 +1,2 @@
export { default as StartRecordingDialog } from './web/StartRecordingDialog';
export { default as RecordingConsentDialog } from './web/RecordingConsentDialog';

View File

@@ -0,0 +1,30 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDialog';
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
/**
* Component that renders the dialog for explicit consent for recordings.
*
* @returns {JSX.Element}
*/
export default function RecordingConsentDialog() {
const dispatch = useDispatch();
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
return true;
}, []);
return (
<ConfirmDialog
confirmLabel = { 'dialog.Understand' }
descriptionKey = { 'dialog.recordingInProgressDescription' }
isCancelHidden = { true }
onSubmit = { consent }
title = { 'dialog.recordingInProgressTitle' } />
);
}

View File

@@ -0,0 +1,36 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
import Dialog from '../../../../base/ui/components/web/Dialog';
/**
* Component that renders the dialog for explicit consent for recordings.
*
* @returns {JSX.Element}
*/
export default function RecordingConsentDialog() {
const { t } = useTranslation();
const dispatch = useDispatch();
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
}, []);
return (
<Dialog
cancel = {{ hidden: true }}
disableBackdropClose = { true }
disableEscape = { true }
hideCloseButton = { true }
ok = {{ translationKey: 'dialog.Understand' }}
onSubmit = { consent }
titleKey = 'dialog.recordingInProgressTitle'>
<div>
{t('dialog.recordingInProgressDescription')}
</div>
</Dialog>
);
}

View File

@@ -11,6 +11,7 @@ import {
isLocalParticipantModerator
} from '../base/participants/functions';
import { registerSound, unregisterSound } from '../base/sounds/actions';
import { isSpotTV } from '../base/util/spot';
import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functions';
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
@@ -437,3 +438,35 @@ export function isLiveStreamingButtonVisible({
}) {
return !isInBreakoutRoom && liveStreamingEnabled && liveStreamingAllowed;
}
/**
* Whether the RecordingConsentDialog should be displayed.
*
* @param {any} recorderSession - The recorder session.
* @param {IReduxState} state - The Redux state.
* @returns {boolean}
*/
export function shouldRequireRecordingConsent(recorderSession: any, state: IReduxState) {
const { requireRecordingConsent } = state['features/dynamic-branding'] || {};
const { requireConsent } = state['features/base/config'].recordings || {};
const { iAmRecorder } = state['features/base/config'];
if (iAmRecorder) {
return false;
}
if (isSpotTV()) {
return false;
}
if (!requireConsent && !requireRecordingConsent) {
return false;
}
if (!recorderSession.getInitiator()
|| recorderSession.getStatus() === JitsiRecordingConstants.status.OFF) {
return false;
}
return recorderSession.getInitiator() !== getLocalParticipant(state)?.id;
}

View File

@@ -1,13 +1,22 @@
import { batch } from 'react-redux';
import { createRecordingEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { openDialog } from '../base/dialog/actions';
import JitsiMeetJS, {
JitsiConferenceEvents,
JitsiRecordingConstants
} from '../base/lib-jitsi-meet';
import {
setAudioMuted,
setAudioUnmutePermissions,
setVideoMuted,
setVideoUnmutePermissions
} from '../base/media/actions';
import { MEDIA_TYPE } from '../base/media/constants';
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import { updateLocalRecordingStatus } from '../base/participants/actions';
@@ -37,6 +46,7 @@ import {
showStoppedRecordingNotification,
updateRecordingSessionData
} from './actions';
import { RecordingConsentDialog } from './components/Recording';
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
import {
LIVE_STREAMING_OFF_SOUND_ID,
@@ -49,6 +59,7 @@ import {
getResourceId,
getSessionById,
registerRecordingAudioFiles,
shouldRequireRecordingConsent,
unregisterRecordingAudioFiles
} from './functions';
import logger from './logger';
@@ -101,7 +112,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
(recorderSession: any) => {
if (recorderSession) {
recorderSession.getID() && dispatch(updateRecordingSessionData(recorderSession));
recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch, getState);
if (recorderSession.getError()) {
_showRecordingErrorNotification(recorderSession, dispatch, getState);
} else {
_showExplicitConsentDialog(recorderSession, dispatch, getState);
}
}
return;
@@ -390,3 +405,25 @@ function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatc
APP.API.notifyRecordingStatusChanged(false, mode, error, isRecorderTranscriptionsRunning(getState()));
}
}
/**
* Mutes audio and video and displays the RecordingConsentDialog when the conditions are met.
*
* @param {any} recorderSession - The recording session.
* @param {Function} dispatch - The Redux dispatch function.
* @param {Function} getState - The Redux getState function.
* @returns {void}
*/
function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
if (!shouldRequireRecordingConsent(recorderSession, getState())) {
return;
}
batch(() => {
dispatch(setAudioUnmutePermissions(true, true));
dispatch(setVideoUnmutePermissions(true, true));
dispatch(setAudioMuted(true));
dispatch(setVideoMuted(true));
dispatch(openDialog(RecordingConsentDialog));
});
}

View File

@@ -200,8 +200,14 @@ end
-- Managing breakout rooms
function create_breakout_room(room_jid, subject)
local main_room, main_room_jid = get_main_room(room_jid);
function create_breakout_room(orig_room, subject)
local main_room, main_room_jid = get_main_room(orig_room.jid);
if orig_room ~= main_room then
module:log('warn', 'Invalid create breakout room request for %s', orig_room.jid);
return;
end
local breakout_room_jid = uuid_gen() .. '@' .. breakout_rooms_muc_component_config;
if not main_room._data.breakout_rooms then
@@ -219,13 +225,18 @@ function create_breakout_room(room_jid, subject)
broadcast_breakout_rooms(main_room_jid);
end
function destroy_breakout_room(room_jid, message)
function destroy_breakout_room(orig_room, room_jid, message)
local main_room, main_room_jid = get_main_room(room_jid);
if room_jid == main_room_jid then
return;
end
if orig_room ~= main_room then
module:log('warn', 'Invalid destroy breakout room request for %s', orig_room.jid);
return;
end
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(room_jid);
if breakout_room then
@@ -244,13 +255,18 @@ function destroy_breakout_room(room_jid, message)
end
function rename_breakout_room(room_jid, name)
function rename_breakout_room(orig_room, room_jid, name)
local main_room, main_room_jid = get_main_room(room_jid);
if room_jid == main_room_jid then
return;
end
if orig_room ~= main_room then
module:log('warn', 'Invalid rename breakout room request for %s', orig_room.jid);
return;
end
if main_room then
if main_room._data.breakout_rooms then
main_room._data.breakout_rooms[room_jid] = name;
@@ -322,18 +338,25 @@ function on_message(event)
end
if message.attr.type == JSON_TYPE_ADD_BREAKOUT_ROOM then
create_breakout_room(room.jid, message.attr.subject);
create_breakout_room(room, message.attr.subject);
return true;
elseif message.attr.type == JSON_TYPE_REMOVE_BREAKOUT_ROOM then
destroy_breakout_room(message.attr.breakoutRoomJid);
destroy_breakout_room(room, message.attr.breakoutRoomJid);
return true;
elseif message.attr.type == JSON_TYPE_RENAME_BREAKOUT_ROOM then
rename_breakout_room(message.attr.breakoutRoomJid, message.attr.subject);
rename_breakout_room(room, message.attr.breakoutRoomJid, message.attr.subject);
return true;
elseif message.attr.type == JSON_TYPE_MOVE_TO_ROOM_REQUEST then
local participant_jid = message.attr.participantJid;
local target_room_jid = message.attr.roomJid;
if not room._data.breakout_rooms or not (
room._data.breakout_rooms[target_room_jid] or target_room_jid == internal_room_jid_match_rewrite(room.jid))
then
module:log('warn', 'Invalid breakout room %s for %s', target_room_jid, room.jid);
return false
end
local json_msg, error = json.encode({
type = BREAKOUT_ROOMS_IDENTITY_TYPE,
event = JSON_TYPE_MOVE_TO_ROOM_REQUEST,
@@ -342,6 +365,7 @@ function on_message(event)
if not json_msg then
module:log('error', 'skip sending request room:%s error:%s', room.jid, error);
return false
end
send_json_msg(participant_jid, json_msg)
@@ -491,7 +515,7 @@ function on_main_room_destroyed(event)
end
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
destroy_breakout_room(breakout_room_jid, event.reason)
destroy_breakout_room(main_room, breakout_room_jid, event.reason)
end
end

View File

@@ -8,6 +8,7 @@ local get_room_from_jid = main_util.get_room_from_jid;
local is_healthcheck_room = main_util.is_healthcheck_room;
local internal_room_jid_match_rewrite = main_util.internal_room_jid_match_rewrite;
local presence_check_status = main_util.presence_check_status;
local extract_subdomain = main_util.extract_subdomain;
local um_is_admin = require 'core.usermanager'.is_admin;
local function is_admin(jid)
@@ -225,7 +226,11 @@ module:hook('message/bare', function(event)
transcription.session_id = room._data.meetingId;
local tenant, conference_name, id = extract_subdomain(jid.node(room.jid));
transcription.fqn = tenant..'/'..conference_name;
if tenant then
transcription.fqn = tenant..'/'..conference_name;
else
transcription.fqn = conference_name;
end
transcription.customer_id = id;
return module:fire_event('jitsi-transcript-received', {

View File

@@ -72,7 +72,7 @@ module:hook('jitsi-endpoint-message-received', function(event)
if string.len(event.raw_message) >= POLL_PAYLOAD_LIMIT then
module:log('error', 'Poll payload too large, discarding. Sender: %s to:%s', stanza.attr.from, stanza.attr.to);
return nil;
return true;
end
if data.type == "new-poll" then
@@ -86,7 +86,7 @@ module:hook('jitsi-endpoint-message-received', function(event)
if room.polls.count >= POLLS_LIMIT then
module:log("error", "Too many polls created in %s", room.jid)
return
return true;
end
if room.polls.by_id[data.pollId] ~= nil then

View File

@@ -280,6 +280,11 @@ function extract_subdomain(room_node)
end
local subdomain, room_name = room_node:match("^%[([^%]]+)%](.+)$");
if not subdomain then
room_name = room_node;
end
local _, customer_id = subdomain and subdomain:match("^(vpaas%-magic%-cookie%-)(.*)$") or nil, nil;
local cache_value = { subdomain=subdomain, room=room_name, customer_id=customer_id };
extract_subdomain_cache:set(room_node, cache_value);

View File

@@ -2,6 +2,9 @@
# If there is a tenant in the URL it must end with a slash (e.g. "https://alpha.jitsi.net/sometenant/")
#BASE_URL=
# Room name suffix to use when creating new room names
# ROOM_NAME_SUFFIX=
# To be able to match a domain to a specific address
# The format is "MAP example.com 1.2.3.4"
#RESOLVER_RULES=
@@ -15,8 +18,8 @@
# The path to the browser video capture file
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
# The path to the helper iframe page that will be used for the iframeAPI tests
#IFRAME_PAGE_BASE=
# The tenant used when executing the iframeAPI tests, will override any tenant from BASE_URL if any
#IFRAME_TENANT=
# The grid host url (https://mygrid.com/wd/hub)
#GRID_HOST_URL=
@@ -26,6 +29,9 @@
# The kid to use in the token
#JWT_KID=
# An access token to use to create meetings (used for the first participant)
#JWT_ACCESS_TOKEN=
# The count of workers that execute the tests in parallel
# MAX_INSTANCES=1

View File

@@ -60,6 +60,30 @@ export class Participant {
analytics: {
disabled: true
},
// if there is a video file to play, use deployment config,
// otherwise use lower resolution to avoid high CPU usage
constraints: process.env.VIDEO_CAPTURE_FILE ? undefined : {
video: {
height: {
ideal: 360,
max: 360,
min: 180
},
// @ts-ignore
width: {
ideal: 640,
max: 640,
min: 320
},
frameRate: {
max: 30
}
}
},
resolution: process.env.VIDEO_CAPTURE_FILE ? undefined : 360,
requireDisplayName: false,
testing: {
testMode: true
@@ -195,7 +219,9 @@ export class Participant {
// @ts-ignore
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${ctx.roomName}"`;
if (baseUrl.pathname.length > 1) {
if (process.env.IFRAME_TENANT) {
url = `${url}&tenant="${process.env.IFRAME_TENANT}"`;
} else if (baseUrl.pathname.length > 1) {
// remove leading slash
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
}
@@ -206,8 +232,15 @@ export class Participant {
await this.driver.setTimeout({ 'pageLoad': 30000 });
let urlToLoad = url.startsWith('/') ? url.substring(1) : url;
if (options.preferGenerateToken && !ctx.iframeAPI && ctx.isJaasAvailable() && process.env.IFRAME_TENANT) {
// This to enables tests like invite, which can force using the jaas auth instead of the provided token
urlToLoad = `/${process.env.IFRAME_TENANT}/${urlToLoad}`;
}
// drop the leading '/' so we can use the tenant if any
await this.driver.url(url.startsWith('/') ? url.substring(1) : url);
await this.driver.url(urlToLoad);
await this.waitForPageToLoad();
@@ -584,10 +617,17 @@ export class Participant {
// let's give it some time to leave the muc, we redirect after hangup so we should wait for the
// change of url
await this.driver.waitUntil(
async () => current !== await this.driver.getUrl(),
async () => {
const u = await this.driver.getUrl();
// trying to debug some failures of reporting not leaving, where we see the close page in screenshot
console.log(`initialUrl: ${current} currentUrl: ${u}`);
return current !== u;
},
{
timeout: 5000,
timeoutMsg: `${this.name} did not leave the muc in 5s`
timeout: 8000,
timeoutMsg: `${this.name} did not leave the muc in 8s initialUrl: ${current}`
}
);

View File

@@ -165,9 +165,16 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
const p1DisplayName = P1_DISPLAY_NAME;
let token;
// if it is jaas create the first one to be moderator and second not moderator
if (ctx.jwtPrivateKeyPath && !options?.skipFirstModerator) {
token = getModeratorToken(p1DisplayName);
if (!options?.skipFirstModerator) {
// we prioritize the access token when iframe is not used and private key is set,
// otherwise if private key is not specified we use the access token if set
if (process.env.JWT_ACCESS_TOKEN
&& ((ctx.jwtPrivateKeyPath && !ctx.iframeAPI && !options?.preferGenerateToken)
|| !ctx.jwtPrivateKeyPath)) {
token = process.env.JWT_ACCESS_TOKEN;
} else if (ctx.jwtPrivateKeyPath) {
token = getModeratorToken(p1DisplayName);
}
}
// make sure the first participant is moderator, if supported by deployment

View File

@@ -7,6 +7,7 @@ export type IContext = {
conferenceJid: string;
dialInPin: string;
iframeAPI: boolean;
isJaasAvailable: () => boolean;
jwtKid: string;
jwtPrivateKeyPath: string;
keepAlive: Array<any>;
@@ -33,6 +34,12 @@ export type IJoinOptions = {
*/
displayName?: string;
/**
* When joining the first participant and jwt singing material is available and a provided token
* is available, prefer generating a new token for the first participant.
*/
preferGenerateToken?: boolean;
/**
* Whether to skip setting display name.
*/

View File

@@ -25,6 +25,6 @@ export default class LobbyScreen extends PreMeetingScreen {
* Waits for lobby screen to load.
*/
waitForLoading(): Promise<void> {
return this.participant.driver.$('.lobby-screen').waitForDisplayed({ timeout: 4000 });
return this.participant.driver.$('.lobby-screen').waitForDisplayed({ timeout: 6000 });
}
}

View File

@@ -17,6 +17,7 @@ export default class PasswordDialog extends BaseDialog {
timeoutMsg: 'Password dialog not found'
});
await input.waitForDisplayed();
await input.waitForStable();
}
/**

View File

@@ -44,7 +44,7 @@ export default abstract class PreMeetingScreen extends BasePageObject {
return this.participant.driver.waitUntil(
() => this.isLobbyRoomJoined(),
{
timeout: 3_000, // 3 seconds
timeout: 6_000, // 6 seconds
timeoutMsg: `Timeout waiting to join lobby for ${this.participant.name}`
}
);

View File

@@ -1,7 +1,7 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Audio only', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx, { skipFirstModerator: true }));
it('joining the meeting', () => ensureTwoParticipants(ctx));
/**
* Enables audio only mode for p1 and verifies that the other participant sees participant1 as video muted.

View File

@@ -37,6 +37,13 @@ describe('Participants presence', () => {
const { p1, p2, webhooksProxy } = ctx;
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
return;
}
// let's populate endpoint ids
await Promise.all([
p1.getEndpointId(),

View File

@@ -1,4 +1,5 @@
import { ensureOneParticipant, ensureTwoParticipants, joinSecondParticipant } from '../../helpers/participants';
import type SecurityDialog from '../../pageobjects/SecurityDialog';
/**
* 1. Lock the room (make sure the image changes to locked)
@@ -30,6 +31,9 @@ describe('Lock Room', () => {
await p2PasswordDialog.waitForDialog();
await p2PasswordDialog.submitPassword(`${ctx.roomKey}1234`);
// give sometime to the password prompt to disappear and send the password
await p2.driver.pause(500);
// wait for password prompt
await p2PasswordDialog.waitForDialog();
await p2PasswordDialog.submitPassword(ctx.roomKey);
@@ -41,7 +45,7 @@ describe('Lock Room', () => {
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
expect(await p2SecurityDialog.isLocked()).toBe(true);
await waitForRoomLockState(p2SecurityDialog, true);
});
it('unlock room', async () => {
@@ -63,7 +67,7 @@ describe('Lock Room', () => {
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
expect(await p2SecurityDialog.isLocked()).toBe(false);
await waitForRoomLockState(p2SecurityDialog, false);
await p2SecurityDialog.clickCloseButton();
});
@@ -79,11 +83,11 @@ describe('Lock Room', () => {
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
expect(await p2SecurityDialog.isLocked()).toBe(true);
await waitForRoomLockState(p2SecurityDialog, true);
await participant1UnlockRoom();
expect(await p2SecurityDialog.isLocked()).toBe(false);
await waitForRoomLockState(p2SecurityDialog, false);
});
it('unlock after participant enter wrong password', async () => {
// P1 locks the room. Participant tries to enter using wrong password.
@@ -104,6 +108,9 @@ describe('Lock Room', () => {
await p2PasswordDialog.waitForDialog();
await p2PasswordDialog.submitPassword(`${ctx.roomKey}1234`);
// give sometime to the password prompt to disappear and send the password
await p2.driver.pause(500);
// wait for password prompt
await p2PasswordDialog.waitForDialog();
@@ -117,7 +124,7 @@ describe('Lock Room', () => {
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
expect(await p2SecurityDialog.isLocked()).toBe(false);
await waitForRoomLockState(p2SecurityDialog, false);
});
});
@@ -133,7 +140,7 @@ async function participant1LockRoom() {
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
expect(await p1SecurityDialog.isLocked()).toBe(false);
await waitForRoomLockState(p1SecurityDialog, false);
await p1SecurityDialog.addPassword(ctx.roomKey);
@@ -142,7 +149,7 @@ async function participant1LockRoom() {
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
expect(await p1SecurityDialog.isLocked()).toBe(true);
await waitForRoomLockState(p1SecurityDialog, true);
await p1SecurityDialog.clickCloseButton();
}
@@ -159,13 +166,22 @@ async function participant1UnlockRoom() {
await p1SecurityDialog.removePassword();
await p1.driver.waitUntil(
async () => !await p1SecurityDialog.isLocked(),
{
timeout: 3_000, // 3 seconds
timeoutMsg: `Timeout waiting for the room to unlock for ${p1.name}.`
}
);
await waitForRoomLockState(p1SecurityDialog, false);
await p1SecurityDialog.clickCloseButton();
}
/**
* Waits for the room to be locked or unlocked.
* @param securityDialog
* @param locked
*/
function waitForRoomLockState(securityDialog: SecurityDialog, locked: boolean) {
return securityDialog.participant.driver.waitUntil(
async () => await securityDialog.isLocked() === locked,
{
timeout: 3_000, // 3 seconds
timeoutMsg: `Timeout waiting for the room to unlock for ${securityDialog.participant.name}.`
}
);
}

View File

@@ -77,6 +77,11 @@ describe('AVModeration', () => {
});
it('hangup and change moderator', async () => {
// no moderator switching if jaas is available
if (ctx.isJaasAvailable()) {
return;
}
await Promise.all([ ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureThreeParticipants(ctx);

View File

@@ -10,10 +10,7 @@ const HASH = '38f014e4b7dde0f64f8157d26a8c812e';
describe('Avatar', () => {
it('setup the meeting', () =>
ensureTwoParticipants(ctx, {
skipDisplayName: true,
// no default avatar if we have used to join a token with an avatar and no option to set it
skipFirstModerator: true
skipDisplayName: true
})
);
@@ -204,8 +201,7 @@ describe('Avatar', () => {
await p1.hangup();
await ensureTwoParticipants(ctx, {
skipDisplayName: true,
skipFirstModerator: true
skipDisplayName: true
});
p1 = ctx.p1;

View File

@@ -34,7 +34,7 @@ describe('BreakoutRooms', () => {
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'No breakout room added for p1'
});
@@ -42,7 +42,7 @@ describe('BreakoutRooms', () => {
// second participant should also see one breakout room
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'No breakout room seen by p2'
});
});
@@ -54,7 +54,7 @@ describe('BreakoutRooms', () => {
// there should be one breakout room
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 1000,
timeout: 3000,
timeoutMsg: 'No breakout room seen by p1'
});
@@ -80,7 +80,7 @@ describe('BreakoutRooms', () => {
return list[0].name === MAIN_ROOM_NAME;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 did not join breakout room'
});
@@ -95,7 +95,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P2 is not seeing p1 in the breakout room'
});
});
@@ -122,7 +122,7 @@ describe('BreakoutRooms', () => {
return list[0].name !== MAIN_ROOM_NAME;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 did not leave breakout room'
});
@@ -137,7 +137,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 0;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P2 is seeing p1 in the breakout room'
});
});
@@ -152,14 +152,14 @@ describe('BreakoutRooms', () => {
// there should be no breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 0, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'Breakout room was not removed for p1'
});
// the second participant should also see no breakout rooms
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'Breakout room was not removed for p2'
});
});
@@ -176,7 +176,7 @@ describe('BreakoutRooms', () => {
// there should be two breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 2, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'Breakout room was not created by p1'
});
@@ -198,7 +198,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1 && list[1].participantCount === 1;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 did not auto assigned participants to breakout rooms'
});
@@ -220,7 +220,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1 && list[1].participantCount === 1
&& (list[0].name === MAIN_ROOM_NAME || list[1].name === MAIN_ROOM_NAME);
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P2 is not seeing p1 in the main room'
});
});
@@ -244,7 +244,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1 && list[1].participantCount === 1;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P1 is not seeing two breakout rooms'
});
@@ -266,7 +266,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 0 || list[1].participantCount === 0;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 is not seeing an empty breakout room'
});
@@ -305,7 +305,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount + list[1].participantCount === 1;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
});
@@ -335,7 +335,7 @@ describe('BreakoutRooms', () => {
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1
&& (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'No breakout room added for p1'
});
@@ -353,7 +353,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
});
});
@@ -373,7 +373,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
});
@@ -419,7 +419,7 @@ describe('BreakoutRooms', () => {
return list[0].name === myNewRoomName;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'The breakout room was not renamed for p1'
});
@@ -441,7 +441,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 0;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'The breakout room not found or not empty for p1'
});
@@ -471,7 +471,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'The breakout room was not rename for p1'
});

View File

@@ -195,6 +195,10 @@ describe('Lobby', () => {
});
it('change of moderators in lobby', async () => {
// no moderator switching if jaas is available
if (ctx.isJaasAvailable()) {
return;
}
await hangupAllParticipants();
await ensureTwoParticipants(ctx);
@@ -222,11 +226,11 @@ describe('Lobby', () => {
// here the important check is whether the moderator sees the knocking participant
await enterLobby(p2, false);
await hangupAllParticipants();
});
it('shared password', async () => {
await hangupAllParticipants();
await ensureTwoParticipants(ctx);
const { p1 } = ctx;
@@ -240,7 +244,7 @@ describe('Lobby', () => {
expect(await p1SecurityDialog.isLocked()).toBe(false);
const roomPasscode = String(Math.random() * 1_000);
const roomPasscode = String(Math.trunc(Math.random() * 1_000_000));
await p1SecurityDialog.addPassword(roomPasscode);
@@ -283,6 +287,10 @@ describe('Lobby', () => {
});
it('moderator leaves while lobby enabled', async () => {
// no moderator switching if jaas is available
if (ctx.isJaasAvailable()) {
return;
}
const { p1, p2, p3 } = ctx;
await p3.hangup();
@@ -302,7 +310,7 @@ describe('Lobby', () => {
});
it('reject and approve in pre-join', async () => {
await ctx.p2.hangup();
await hangupAllParticipants();
await ensureTwoParticipants(ctx);
await enableLobby();
@@ -471,8 +479,12 @@ async function enterLobby(participant: Participant, enterDisplayName = false, us
await screen.waitToJoinLobby();
// check no join button
expect(!await joinButton.isExisting() || !await joinButton.isDisplayed() || !await joinButton.isEnabled())
.toBe(true);
await p3.driver.waitUntil(
async () => !await joinButton.isExisting() || !await joinButton.isDisplayed() || !await joinButton.isEnabled(),
{
timeout: 2_000,
timeoutMsg: 'Join button is still available for p3'
});
// new screen, is password button shown
const passwordButton = screen.getPasswordButton();

View File

@@ -9,7 +9,16 @@ import {
describe('StartMuted', () => {
it('checkboxes test', async () => {
const options = { configOverwrite: { p2p: { enabled: true } } };
const options = {
configOverwrite: {
p2p: {
enabled: true
},
testing: {
testMode: true,
debugAudioLevels: true
}
} };
await ensureOneParticipant(ctx, options);
@@ -162,7 +171,11 @@ describe('StartMuted', () => {
const options = {
configOverwrite: {
startWithAudioMuted: true
startWithAudioMuted: true,
testing: {
testMode: true,
debugAudioLevels: true
}
}
};
@@ -188,17 +201,19 @@ describe('StartMuted', () => {
}
};
await Promise.all([
ensureOneParticipant(ctx, options),
joinSecondParticipant(ctx, {
configOverwrite: {
p2p: {
enabled: true
}
await ensureOneParticipant(ctx, options);
await joinSecondParticipant(ctx, {
configOverwrite: {
testing: {
testMode: true,
debugAudioLevels: true
},
skipInMeetingChecks: true
})
]);
p2p: {
enabled: true
}
},
skipInMeetingChecks: true
});
const { p1, p2 } = ctx;

View File

@@ -2,7 +2,7 @@ import { ensureOneParticipant } from '../../helpers/participants';
import { isDialInEnabled } from '../helpers/DialIn';
describe('Invite', () => {
it('join participant', () => ensureOneParticipant(ctx));
it('join participant', () => ensureOneParticipant(ctx, { preferGenerateToken: true }));
it('url displayed', async () => {
const { p1 } = ctx;

View File

@@ -194,7 +194,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
// setup keepalive
globalAny.ctx.keepAlive.push(setInterval(async () => {
await bInstance.execute(() => console.log('keep-alive'));
await bInstance.execute(() => console.log(`${new Date().toISOString()} keep-alive`));
}, 20_000));
if (bInstance.isFirefox) {
@@ -208,8 +208,13 @@ export const config: WebdriverIO.MultiremoteConfig = {
}));
globalAny.ctx.roomName = `jitsimeettorture-${crypto.randomUUID()}`;
if (process.env.ROOM_NAME_SUFFIX) {
globalAny.ctx.roomName += `_${process.env.ROOM_NAME_SUFFIX.trim()}`;
}
globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
globalAny.ctx.jwtKid = process.env.JWT_KID;
globalAny.ctx.isJaasAvailable = () => globalAny.ctx.jwtKid?.startsWith('vpaas-magic-cookie-');
},
after() {