mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-27 17:10:20 +00:00
Compare commits
20 Commits
dependabot
...
release-84
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39c1542286 | ||
|
|
bf39fff501 | ||
|
|
08d5466e5e | ||
|
|
6966fa9eca | ||
|
|
1f2d999459 | ||
|
|
c84f92075d | ||
|
|
f506f85864 | ||
|
|
4bae526610 | ||
|
|
c13fdf7913 | ||
|
|
cdcdd1b1bb | ||
|
|
9017b24fee | ||
|
|
88cb4d4e00 | ||
|
|
a88203cf03 | ||
|
|
7455fe97fa | ||
|
|
0520e01194 | ||
|
|
5524745099 | ||
|
|
1d1c7e2e31 | ||
|
|
c9dd4d0f4d | ||
|
|
dbc06bd75a | ||
|
|
7554dcad97 |
11
config.js
11
config.js
@@ -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"
|
||||
|
||||
@@ -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
9
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -205,7 +205,10 @@ export default [
|
||||
'remoteVideoMenu',
|
||||
'roomPasswordNumberOfDigits',
|
||||
'readOnlyName',
|
||||
'recordings',
|
||||
'recordings.recordAudioAndVideo',
|
||||
'recordings.showPrejoinWarning',
|
||||
'recordings.showRecordingLink',
|
||||
'recordings.suggestRecording',
|
||||
'replaceParticipant',
|
||||
'resolution',
|
||||
'screenshotCapture',
|
||||
|
||||
@@ -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') }
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
14
react/features/base/ui/jitsiTokens.json
Normal file
14
react/features/base/ui/jitsiTokens.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"success05": "#1EC26A",
|
||||
|
||||
"support05": "#73348C",
|
||||
"support06": "#6A50D3",
|
||||
|
||||
"surface01": "#040404",
|
||||
"surface02": "#141414",
|
||||
"surface03": "#292929",
|
||||
"surface05": "#525252",
|
||||
"surface08": "#A3A3A3",
|
||||
|
||||
"warning06": "#FFD600"
|
||||
}
|
||||
211
react/features/base/ui/tokens.json
Normal file
211
react/features/base/ui/tokens.json
Normal 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"
|
||||
}
|
||||
@@ -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 });
|
||||
}, {});
|
||||
}
|
||||
|
||||
9
react/features/base/util/spot.ts
Normal file
9
react/features/base/util/spot.ts
Normal 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/');
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 } />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as StartRecordingDialog } from './native/StartRecordingDialog';
|
||||
export { default as RecordingConsentDialog } from './native/RecordingConsentDialog';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as StartRecordingDialog } from './web/StartRecordingDialog';
|
||||
export { default as RecordingConsentDialog } from './web/RecordingConsentDialog';
|
||||
|
||||
@@ -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' } />
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class PasswordDialog extends BaseDialog {
|
||||
timeoutMsg: 'Password dialog not found'
|
||||
});
|
||||
await input.waitForDisplayed();
|
||||
await input.waitForStable();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}.`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user