Compare commits

...

28 Commits

Author SHA1 Message Date
Hristo Terezov
61d483ce1a fix(trackOpQueue):setEffect &_turnScreenSharingOff 2022-12-12 15:53:18 -06:00
Hristo Terezov
1e2f9160b5 fix: review comments. 2022-12-07 18:27:26 -06:00
Hristo Terezov
4daddd341d docs(conference.js): Add JSDoc for startConference 2022-12-07 14:37:56 -06:00
Hristo Terezov
15cd83387a fix(conference):useAudioVideo remove unused promise 2022-12-07 10:04:29 -06:00
Hristo Terezov
8303e261b2 fix: review comments 2022-12-07 09:56:11 -06:00
Hristo Terezov
d3c45a5dea fix(conference): started muted from jicofo 2022-12-07 09:56:11 -06:00
Hristo Terezov
0d6f00abf3 feat(tracks): Synchronize track operations. 2022-12-07 09:56:09 -06:00
William Liang
83dfb67f23 fix(video-mute) prevent multiple camera track creation 2022-12-07 10:23:20 -05:00
Robert Pintilii
51bdf67cf2 fix(dialog) Fix Dialog on mobile (#12650)
Use JitsiPortal on mobile
2022-12-07 11:27:55 +02:00
damencho
3adbda791c chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1548.0.0+8acdeb1d...v1549.0.0+877c4546
2022-12-06 19:10:02 -06:00
Jaya Allamsetty
924bb0e7ff chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1545.0.0+f8e587f7...v1548.0.0+8acdeb1d
2022-12-06 16:31:06 -06:00
tmoldovan8x8
4c9bfe3d4d feat(E2EE) add initial SAS verification UI 2022-12-06 18:29:33 +01:00
Jaya Allamsetty
1139311809 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1543.0.0+c57ac97e...v1545.0.0+f8e587f7
2022-12-06 10:53:20 -05:00
Calinteodor
2ad2e6ff0e feat(polls/web): removed sort options from polls (#12641)
* feat(polls/web): removed sort options from polls
2022-12-05 15:18:24 +02:00
damencho
46cc2e37ae feat: Update lib-jitsi-meet.
Fixes version of binary ljm.
2022-12-02 15:31:33 -06:00
Saúl Ibarra Corretgé
0ebac2ac6d fixup! 2022-12-02 19:05:47 +01:00
Saúl Ibarra Corretgé
90e33ee799 fixup devcontainer 2022-12-02 19:05:47 +01:00
Saúl Ibarra Corretgé
be982ae996 fix(build) use http for GitHub codespaces 2022-12-02 19:05:47 +01:00
Saúl Ibarra Corretgé
56114fe863 Create devcontainer.json 2022-12-02 19:05:47 +01:00
Saúl Ibarra Corretgé
a2e8a7f28f fix(toolbox) hide drawer after toggling camera
Fixes: https://github.com/jitsi/jitsi-meet/issues/12628
2022-12-02 15:31:44 +01:00
Pawel Domas
af072c3070 fix: get the current conference
state['features/base/conference'].conference is not the right way to get the current conference.
See getCurrentConference selector - it accounts for joining and other states.
2022-12-01 16:02:35 -05:00
Jaya Allamsetty
f42772ec5b chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1539.0.0+eb4873d2...v1541.0.0+9b34e0f7
2022-11-29 15:28:51 -05:00
Gabriel Borlea
2556a7ab77 fix(face-landmarks): assign empty array to facelandmarks in speakerstats if undefined 2022-11-29 19:12:43 +01:00
Charles Zablit
3cbf160f2b fix: always display transcription (#12325)
* feat: always display transcription

* fix: unused import
2022-11-29 09:50:19 -06:00
Дамян Минков
744960bb1a feat: Several module optimizations to avoid constant parsing of jids. (#12594)
* feat: Several module optimizations to avoid constant parsing of jids.

Caches the parsed values in a rotating table with limited size.
Skips constant creating of a stanza with never changing values - create it once and then just clone it.

* squash: Fixes extract_subdomain multiple values.

* squash: Fix table values when there is a nil element.

* squash: Fix skipping the roomless IQs.

* squash: Fix comments.
2022-11-28 14:18:59 -06:00
Дамян Минков
76471a0ea9 feat: Modules for implementing visitor nodes. (#12593)
* feat: Modules for implementing visitor nodes.

Still WIP, uses visitor nodes prosodies where we create the main participants and forward the visitors to watch. Used for huge conferences.

* squash: Fix comments.
2022-11-28 14:18:33 -06:00
Robert Pintilii
0ba033e07d ref(TS) Improve TS (#12612)
Remove unnecessary @ts-ignores
Remove unnecessary eslint-disable
2022-11-28 12:52:45 +02:00
Robert Pintilii
cb3fb3ada9 ref(TS) Convert some features to TS (#12611) 2022-11-28 12:52:24 +02:00
164 changed files with 2391 additions and 1432 deletions

View File

@@ -0,0 +1,15 @@
{
"name": "Jitsi Meet Dev Container",
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "16"
}
},
"hostRequirements": {
"cpus": 4,
"memory": "8gb",
"storage": "32gb"
},
"postCreateCommand": "bash -i -c 'nvm use && npm install && cp tsconfig.web.json tsconfig.json'"
}

File diff suppressed because it is too large Load Diff

View File

@@ -121,13 +121,6 @@ ol.poll-result-list {
display: flex;
}
.poll-dragged {
opacity: 0.5;
* {
cursor: grabbing !important;
}
}
.poll-question {
font-size: 16px;
font-weight: 600;

1
globals.d.ts vendored
View File

@@ -14,6 +14,7 @@ declare global {
registerShortcut: Function;
unregisterShortcut: Function;
openDialog: Function;
enable: Function;
}
};
const interfaceConfig: any;

View File

@@ -147,6 +147,7 @@
"bridgeCount": "Server count: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Connected to:",
"e2eeVerified": "E2EE verified:",
"framerate": "Frame rate:",
"less": "Show less",
"localaddress": "Local address:",
@@ -408,6 +409,10 @@
"user": "User",
"userIdentifier": "User identifier",
"userPassword": "User password",
"verifyParticipantConfirm": "They match",
"verifyParticipantDismiss": "They do not match",
"verifyParticipantQuestion": "EXPERIMENTAL: Ask participant {{participantName}} if they see the same content, in the same order.",
"verifyParticipantTitle": "User verification",
"videoLink": "Video link",
"viewUpgradeOptions": "View upgrade options",
"viewUpgradeOptionsContent": "To get unlimited access to premium features like recording, transcriptions, RTMP Streaming & more, you'll need to upgrade your plan.",
@@ -1297,6 +1302,7 @@
"show": "Show on stage",
"showSelfView": "Show self view",
"unpinFromStage": "Unpin",
"verify": "Verify participant",
"videoMuted": "Camera disabled",
"videomute": "Participant has stopped the camera"
},

View File

@@ -23,6 +23,7 @@ import {
getVideoTrackByParticipant,
trackStreamingStatusChanged
} from '../../../react/features/base/tracks';
import { createDeferred } from '../../../react/features/base/util/helpers';
import { CHAT_SIZE } from '../../../react/features/chat';
import {
isTrackStreamingStatusActive,
@@ -38,7 +39,6 @@ import { getParticipantsPaneOpen } from '../../../react/features/participants-pa
import { PresenceLabel } from '../../../react/features/presence-status';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import { createDeferred } from '../../util/helpers';
import AudioLevels from '../audio_levels/AudioLevels';
import { VIDEO_CONTAINER_TYPE, VideoContainer } from './VideoContainer';

View File

@@ -1,71 +0,0 @@
const logger = require('@jitsi/logger').getLogger(__filename);
/**
* Manages a queue of functions where the current function in progress will
* automatically execute the next queued function.
*/
export class TaskQueue {
/**
* Creates a new instance of {@link TaskQueue} and sets initial instance
* variable values.
*/
constructor() {
this._queue = [];
this._currentTask = null;
this._onTaskComplete = this._onTaskComplete.bind(this);
}
/**
* Adds a new function to the queue. It will be immediately invoked if no
* other functions are queued.
*
* @param {Function} taskFunction - The function to be queued for execution.
* @private
* @returns {void}
*/
enqueue(taskFunction) {
this._queue.push(taskFunction);
this._executeNext();
}
/**
* If no queued task is currently executing, invokes the first task in the
* queue if any.
*
* @private
* @returns {void}
*/
_executeNext() {
if (this._currentTask) {
logger.warn('Task queued while a task is in progress.');
return;
}
this._currentTask = this._queue.shift() || null;
if (this._currentTask) {
logger.debug('Executing a task.');
try {
this._currentTask(this._onTaskComplete);
} catch (error) {
logger.error(`Task execution failed: ${error}`);
this._onTaskComplete();
}
}
}
/**
* Prepares to invoke the next function in the queue.
*
* @private
* @returns {void}
*/
_onTaskComplete() {
this._currentTask = null;
logger.debug('Task completed.');
this._executeNext();
}
}

View File

@@ -1,26 +0,0 @@
import { TaskQueue } from './TaskQueue';
/**
* Create deferred object.
*
* @returns {{promise, resolve, reject}}
*/
export function createDeferred() {
const deferred = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
}
/**
* Returns an instance of {@link TaskQueue}.
*
* @returns {Object}
*/
export function createTaskQueue() {
return new TaskQueue();
}

10
package-lock.json generated
View File

@@ -74,7 +74,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/v1539.0.0+eb4873d2/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1549.0.0+877c4546/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -13497,8 +13497,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1539.0.0+eb4873d2/lib-jitsi-meet.tgz",
"integrity": "sha512-hEu5nmljbOVKPHIcCpW6GTFzZpWDOAfplKTkNdvrIyNgMiIjHhUgmMzF+IGicd7KPud3z8aQ+0HFnpcxO/T6ZA==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1549.0.0+877c4546/lib-jitsi-meet.tgz",
"integrity": "sha512-cxzr8vnJ6RyqWYzJ4LO09PqblJ6nIrJFFmzW8kPQgC1Nhq7sDAD896827q/shd+FE6bSoK0xVjDP+gW+JInS9A==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@@ -30510,8 +30510,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1539.0.0+eb4873d2/lib-jitsi-meet.tgz",
"integrity": "sha512-hEu5nmljbOVKPHIcCpW6GTFzZpWDOAfplKTkNdvrIyNgMiIjHhUgmMzF+IGicd7KPud3z8aQ+0HFnpcxO/T6ZA==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1549.0.0+877c4546/lib-jitsi-meet.tgz",
"integrity": "sha512-cxzr8vnJ6RyqWYzJ4LO09PqblJ6nIrJFFmzW8kPQgC1Nhq7sDAD896827q/shd+FE6bSoK0xVjDP+gW+JInS9A==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",

View File

@@ -79,7 +79,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/v1539.0.0+eb4873d2/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1549.0.0+877c4546/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -1,5 +1,3 @@
/* eslint-disable lines-around-comment */
import logger from '../logger';
import AbstractHandler, { IEvent } from './AbstractHandler';
@@ -103,8 +101,10 @@ export default class AmplitudeHandler extends AbstractHandler {
return {
sessionId: amplitude.getInstance().getSessionId(),
// @ts-ignore
deviceId: amplitude.getInstance().options.deviceId,
// @ts-ignore
userId: amplitude.getInstance().options.userId
};

View File

@@ -24,7 +24,7 @@ import { IResponsiveUIState } from '../base/responsive-ui/reducer';
import { ISettingsState } from '../base/settings/reducer';
import { ISoundsState } from '../base/sounds/reducer';
import { ITestingState } from '../base/testing/reducer';
import { INoSrcDataState, ITracksState } from '../base/tracks/reducer';
import { INoSrcDataState, ITrackOperations, ITracksState } from '../base/tracks/reducer';
import { IUserInteractionState } from '../base/user-interaction/reducer';
import { IBreakoutRoomsState } from '../breakout-rooms/reducer';
import { ICalendarSyncState } from '../calendar-sync/reducer';
@@ -107,6 +107,7 @@ export interface IReduxState {
'features/base/responsive-ui': IResponsiveUIState;
'features/base/settings': ISettingsState;
'features/base/sounds': ISoundsState;
'features/base/track-operations': ITrackOperations;
'features/base/tracks': ITracksState;
'features/base/user-interaction': IUserInteractionState;
'features/breakout-rooms': IBreakoutRoomsState;

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import { createStartMutedConfigurationEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { appNavigate } from '../../app/actions';
@@ -22,11 +21,13 @@ import { getNormalizedDisplayName } from '../participants/functions';
import { toState } from '../redux/functions';
import {
destroyLocalTracks,
replaceLocalTrack,
executeTrackOperation,
replaceStoredTracks,
trackAdded,
trackRemoved
} from '../tracks/actions.any';
} from '../tracks/actions';
import { getLocalTracks } from '../tracks/functions';
import { TrackOperationType } from '../tracks/types';
import { getBackendSafeRoomName } from '../util/uri';
import {
@@ -111,6 +112,7 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
JitsiConferenceEvents.CONFERENCE_LEFT,
(...args: any[]) => {
dispatch(conferenceTimestampChanged(0));
// @ts-ignore
dispatch(conferenceLeft(conference, ...args));
});
@@ -137,34 +139,43 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
conference.on(
JitsiConferenceEvents.STARTED_MUTED,
() => {
const audioMuted = Boolean(conference.isStartAudioMuted());
const videoMuted = Boolean(conference.isStartVideoMuted());
const localTracks = getLocalTracks(state['features/base/tracks']);
dispatch(executeTrackOperation(TrackOperationType.AudioVideo, () => {
const promises = [];
const audioMuted = Boolean(conference.isStartAudioMuted());
const videoMuted = Boolean(conference.isStartVideoMuted());
const localTracks = getLocalTracks(state['features/base/tracks']);
sendAnalytics(createStartMutedConfigurationEvent('remote', audioMuted, videoMuted));
logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${videoMuted ? 'video' : ''}`);
sendAnalytics(createStartMutedConfigurationEvent('remote', audioMuted, videoMuted));
logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${videoMuted ? 'video' : ''}`);
// XXX Jicofo tells lib-jitsi-meet to start with audio and/or video
// muted i.e. Jicofo expresses an intent. Lib-jitsi-meet has turned
// Jicofo's intent into reality by actually muting the respective
// tracks. The reality is expressed in base/tracks already so what
// is left is to express Jicofo's intent in base/media.
// TODO Maybe the app needs to learn about Jicofo's intent and
// transfer that intent to lib-jitsi-meet instead of lib-jitsi-meet
// acting on Jicofo's intent without the app's knowledge.
dispatch(setAudioMuted(audioMuted));
dispatch(setVideoMuted(videoMuted));
// XXX Jicofo tells lib-jitsi-meet to start with audio and/or video
// muted i.e. Jicofo expresses an intent. Lib-jitsi-meet has turned
// Jicofo's intent into reality by actually muting the respective
// tracks. The reality is expressed in base/tracks already so what
// is left is to express Jicofo's intent in base/media.
// TODO Maybe the app needs to learn about Jicofo's intent and
// transfer that intent to lib-jitsi-meet instead of lib-jitsi-meet
// acting on Jicofo's intent without the app's knowledge.
promises.push(
dispatch(setAudioMuted(audioMuted)).catch(e => logger.error(`Set audio muted failed: ${e}`)));
promises.push(
dispatch(setVideoMuted(videoMuted)).catch(e => logger.error(`Set video muted failed: ${e}`)));
// Remove the tracks from peerconnection as well.
for (const track of localTracks) {
const trackType = track.jitsiTrack.getType();
// Remove the tracks from peerconnection as well.
for (const track of localTracks) {
const trackType = track.jitsiTrack.getType();
// Do not remove the audio track on RN. Starting with iOS 15 it will fail to unmute otherwise.
if ((audioMuted && trackType === MEDIA_TYPE.AUDIO && navigator.product !== 'ReactNative')
|| (videoMuted && trackType === MEDIA_TYPE.VIDEO)) {
dispatch(replaceLocalTrack(track.jitsiTrack, null, conference));
// Do not remove the audio track on RN. Starting with iOS 15 it will fail to unmute otherwise.
if ((audioMuted && trackType === MEDIA_TYPE.AUDIO && navigator.product !== 'ReactNative')
|| (videoMuted && trackType === MEDIA_TYPE.VIDEO)) {
promises.push(
dispatch(replaceStoredTracks(track.jitsiTrack, null))
.catch(e => logger.error(`replaceLocalTrack failed: ${e}`)));
}
}
}
return Promise.all(promises);
}));
});
conference.on(

View File

@@ -59,6 +59,7 @@ export interface IJitsiConference {
grantOwner: Function;
isAVModerationSupported: Function;
isCallstatsEnabled: Function;
isE2EEEnabled: Function;
isEndConferenceSupported: Function;
isLobbySupported: Function;
isSIPCallingSupported: Function;
@@ -71,6 +72,7 @@ export interface IJitsiConference {
muteParticipant: Function;
myLobbyUserId: Function;
myUserId: Function;
off: Function;
on: Function;
removeTrack: Function;
replaceTrack: Function;
@@ -88,6 +90,7 @@ export interface IJitsiConference {
setReceiverConstraints: Function;
setSenderVideoConstraint: Function;
setSubject: Function;
startVerification: Function;
}
export interface IConferenceState {

View File

@@ -316,6 +316,7 @@ export interface IConfig {
sdkKey?: string;
tileTime?: number;
};
googleApiApplicationClientID?: string;
gravatar?: {
baseUrl?: string;
disabled?: boolean;
@@ -366,6 +367,7 @@ export interface IConfig {
localSubject?: string;
locationURL?: URL;
maxFullResolutionParticipants?: number;
microsoftApiApplicationClientID?: string;
moderatedRoomServiceUrl?: string;
mouseMoveCallbackInterval?: number;
noticeMessage?: string;

View File

@@ -1,6 +1,6 @@
/* eslint-disable lines-around-comment */
// @ts-ignore
import Bourne from '@hapi/bourne';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { jitsiLocalStorage } from '@jitsi/js-utils';
import _ from 'lodash';

View File

@@ -77,6 +77,7 @@ export interface IConfigState extends IConfig {
analysis?: {
obfuscateRoomName?: boolean;
};
disableRemoteControl?: boolean;
error?: Error;
}

View File

@@ -16,6 +16,7 @@ import { ConnectionFailedError } from './actions.any';
export interface IConnectionState {
connecting?: any;
connection?: {
addFeature: Function;
disconnect: Function;
getJid: () => string;
getLogs: () => Object;

View File

@@ -4,6 +4,7 @@ import WaitForOwnerDialog from '../../authentication/components/web/WaitForOwner
import ChatPrivacyDialog from '../../chat/components/web/ChatPrivacyDialog';
import DesktopPicker from '../../desktop-picker/components/DesktopPicker';
import DisplayNamePrompt from '../../display-name/components/web/DisplayNamePrompt';
import ParticipantVerificationDialog from '../../e2ee/components/ParticipantVerificationDialog';
import EmbedMeetingDialog from '../../embed-meeting/components/EmbedMeetingDialog';
// @ts-ignore
import FeedbackDialog from '../../feedback/components/FeedbackDialog.web';
@@ -49,7 +50,7 @@ const NEW_DIALOG_LIST = [ KeyboardShortcutsDialog, ChatPrivacyDialog, DisplayNam
SharedVideoDialog, SpeakerStats, LanguageSelectorDialog, MuteEveryoneDialog, MuteEveryonesVideoDialog,
GrantModeratorDialog, KickRemoteParticipantDialog, MuteRemoteParticipantsVideoDialog, VideoQualityDialog,
VirtualBackgroundDialog, LoginDialog, WaitForOwnerDialog, DesktopPicker, RemoteControlAuthorizationDialog,
LogoutDialog, SalesforceLinkDialog ];
LogoutDialog, SalesforceLinkDialog, ParticipantVerificationDialog ];
// This function is necessary while the transition from @atlaskit dialog to our component is ongoing.
const isNewDialog = (component: any) => NEW_DIALOG_LIST.some(comp => comp === component);

View File

@@ -1,8 +1,8 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
// @ts-ignore
import { Container } from '../../react/base';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { styleTypeToObject } from '../../styles';

View File

@@ -2,6 +2,11 @@ import { IStore } from '../../app/types';
import { showModeratedNotification } from '../../av-moderation/actions';
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { isModerationNotificationDisplayed } from '../../notifications/functions';
import { isForceMuted } from '../../participants-pane/functions';
import { maybeStopMuteBecauseOfLocalRecording } from '../../recording/functions';
import { getLocalParticipant } from '../participants/functions';
import { setMuted } from '../tracks/actions.any';
import { isUserInteractionRequiredForUnmute } from '../tracks/functions';
import {
SET_AUDIO_AVAILABLE,
@@ -53,10 +58,32 @@ export function setAudioAvailable(available: boolean) {
* }}
*/
export function setAudioMuted(muted: boolean, ensureTrack = false) {
return {
type: SET_AUDIO_MUTED,
ensureTrack,
muted
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const participant = getLocalParticipant(state);
if (!muted && isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
return Promise.resolve();
}
if (!muted
&& isUserInteractionRequiredForUnmute(state)) {
return Promise.resolve();
}
const mutePromise = dispatch(setMuted({
muted,
ensureTrack,
mediaType: MEDIA_TYPE.AUDIO
}));
dispatch({
type: SET_AUDIO_MUTED,
ensureTrack,
muted
});
return mutePromise;
};
}
@@ -114,7 +141,7 @@ export function setScreenshareMuted(
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.SCREENSHARE));
}
return;
return Promise.resolve();
}
const oldValue = state['features/base/media'].screenshare.muted;
@@ -122,13 +149,28 @@ export function setScreenshareMuted(
// eslint-disable-next-line no-bitwise
const newValue = muted ? oldValue | authority : oldValue & ~authority;
return dispatch({
const participant = getLocalParticipant(state);
if (!newValue && isForceMuted(participant, MEDIA_TYPE.SCREENSHARE, state)) {
return Promise.resolve();
}
const mutePromise = dispatch(setMuted({
authority,
mediaType,
ensureTrack,
muted: Boolean(newValue)
}));
dispatch({
type: SET_SCREENSHARE_MUTED,
authority,
mediaType,
ensureTrack,
muted: newValue
});
return mutePromise;
};
}
@@ -163,7 +205,7 @@ export function setVideoAvailable(available: boolean) {
*/
export function setVideoMuted(
muted: boolean,
mediaType: string = MEDIA_TYPE.VIDEO,
mediaType: MediaType = MEDIA_TYPE.VIDEO,
authority: number = VIDEO_MUTISM_AUTHORITY.USER,
ensureTrack = false) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
@@ -175,21 +217,43 @@ export function setVideoMuted(
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.VIDEO));
}
return;
return Promise.resolve();
}
const oldValue = state['features/base/media'].video.muted;
// eslint-disable-next-line no-bitwise
const newValue = muted ? oldValue | authority : oldValue & ~authority;
const participant = getLocalParticipant(state);
return dispatch({
if (!newValue && isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
return Promise.resolve();
}
if (maybeStopMuteBecauseOfLocalRecording(Boolean(newValue), dispatch)) {
return Promise.resolve();
}
if (!newValue && isUserInteractionRequiredForUnmute(state)) {
return Promise.resolve();
}
const mutePromise = dispatch(setMuted({
ensureTrack,
authority,
muted: Boolean(newValue),
mediaType
}));
dispatch({
type: SET_VIDEO_MUTED,
authority,
mediaType,
ensureTrack,
muted: newValue
});
return mutePromise;
};
}

View File

@@ -12,12 +12,10 @@ import {
NOTIFICATION_TIMEOUT_TYPE,
showWarningNotification
} from '../../notifications';
import { isForceMuted } from '../../participants-pane/functions';
import { isScreenMediaShared } from '../../screen-share/functions';
import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
import { SET_ROOM, isRoomValid } from '../conference';
import { getMultipleVideoSendingSupportFeatureFlag } from '../config';
import { getLocalParticipant } from '../participants';
import { MiddlewareRegistry } from '../redux';
import { getPropertyValue } from '../settings';
import {
@@ -27,12 +25,11 @@ import {
isLocalVideoTrackDesktop,
setTrackMuted
} from '../tracks';
import { executeTrackOperation } from '../tracks/actions';
import { TrackOperationType } from '../tracks/types';
import {
SET_AUDIO_MUTED,
SET_AUDIO_UNMUTE_PERMISSIONS,
SET_SCREENSHARE_MUTED,
SET_VIDEO_MUTED,
SET_VIDEO_UNMUTE_PERMISSIONS
} from './actionTypes';
import {
@@ -54,6 +51,9 @@ import {
_VIDEO_INITIAL_MEDIA_STATE
} from './reducer';
import './subscriber';
/**
* Implements the entry point of the middleware of the feature base/media.
*
@@ -83,16 +83,6 @@ MiddlewareRegistry.register(store => next => action => {
return result;
}
case SET_AUDIO_MUTED: {
const state = store.getState();
const participant = getLocalParticipant(state);
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
return;
}
break;
}
case SET_AUDIO_UNMUTE_PERMISSIONS: {
const { blocked, skipNotification } = action;
const state = store.getState();
@@ -108,25 +98,6 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case SET_SCREENSHARE_MUTED: {
const state = store.getState();
const participant = getLocalParticipant(state);
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.SCREENSHARE, state)) {
return;
}
break;
}
case SET_VIDEO_MUTED: {
const state = store.getState();
const participant = getLocalParticipant(state);
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
return;
}
break;
}
case SET_VIDEO_UNMUTE_PERMISSIONS: {
const { blocked, skipNotification } = action;
const state = store.getState();
@@ -191,9 +162,12 @@ function _setAudioOnly({ dispatch, getState }, next, action) {
sendAnalytics(createTrackMutedEvent('video', 'audio-only mode', audioOnly));
// Make sure we mute both the desktop and video tracks.
dispatch(setVideoMuted(audioOnly, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY));
dispatch(executeTrackOperation(TrackOperationType.Video,
() => dispatch(setVideoMuted(audioOnly, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY))));
if (getMultipleVideoSendingSupportFeatureFlag(state)) {
dispatch(setScreenshareMuted(audioOnly, MEDIA_TYPE.SCREENSHARE, SCREENSHARE_MUTISM_AUTHORITY.AUDIO_ONLY));
dispatch(executeTrackOperation(TrackOperationType.Video,
() => dispatch(setScreenshareMuted(
audioOnly, MEDIA_TYPE.SCREENSHARE, SCREENSHARE_MUTISM_AUTHORITY.AUDIO_ONLY))));
}
return next(action);
@@ -234,9 +208,9 @@ function _setRoom({ dispatch, getState }, next, action) {
// Unconditionally express the desires/expectations/intents of the app and
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
// the state of base/tracks will or will not agree with the desires.
dispatch(setAudioMuted(audioMuted));
dispatch(executeTrackOperation(TrackOperationType.Audio, () => dispatch(setAudioMuted(audioMuted))));
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
dispatch(setVideoMuted(videoMuted));
dispatch(executeTrackOperation(TrackOperationType.Video, () => dispatch(setVideoMuted(videoMuted))));
// startAudioOnly
//
@@ -296,21 +270,29 @@ function _setRoom({ dispatch, getState }, next, action) {
* @private
* @returns {void}
*/
function _syncTrackMutedState({ getState }, track) {
const state = getState()['features/base/media'];
function _syncTrackMutedState({ dispatch, getState }, track) {
const mediaType = track.mediaType;
const muted = Boolean(state[mediaType].muted);
const trackOpType = mediaType === MEDIA_TYPE.AUDIO ? TrackOperationType.Audio : TrackOperationType.Video;
// XXX If muted state of track when it was added is different from our media
// muted state, we need to mute track and explicitly modify 'muted' property
// on track. This is because though TRACK_ADDED action was dispatched it's
// not yet in redux state and JitsiTrackEvents.TRACK_MUTE_CHANGED may be
// fired before track gets to state.
if (track.muted !== muted) {
sendAnalytics(createSyncTrackStateEvent(mediaType, muted));
logger.log(`Sync ${mediaType} track muted state to ${muted ? 'muted' : 'unmuted'}`);
dispatch(executeTrackOperation(trackOpType, () => {
const state = getState()['features/base/media'];
track.muted = muted;
setTrackMuted(track.jitsiTrack, muted, state);
}
const muted = Boolean(state[mediaType].muted);
// XXX If muted state of track when it was added is different from our media
// muted state, we need to mute track and explicitly modify 'muted' property
// on track. This is because though TRACK_ADDED action was dispatched it's
// not yet in redux state and JitsiTrackEvents.TRACK_MUTE_CHANGED may be
// fired before track gets to state.
if (track.muted !== muted) {
sendAnalytics(createSyncTrackStateEvent(track.mediaType, muted));
logger.log(`Sync ${track.mediaType} track muted state to ${muted ? 'muted' : 'unmuted'}`);
track.muted = muted;
return setTrackMuted(track.jitsiTrack, muted, state);
}
return Promise.resolve();
}));
}

View File

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

View File

@@ -1,45 +0,0 @@
import './middleware.any.js';
import { IStore } from '../../app/types';
import { showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager.web';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import StopRecordingDialog from '../../recording/components/Recording/web/StopRecordingDialog';
import { openDialog } from '../dialog/actions';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { SET_VIDEO_MUTED } from './actionTypes';
import './subscriber';
/**
* Implements the entry point of the middleware of the feature base/media.
*
* @param {IStore} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any) => {
const { dispatch } = store;
switch (action.type) {
case SET_VIDEO_MUTED: {
if (LocalRecordingManager.isRecordingLocally() && LocalRecordingManager.selfRecording.on) {
if (action.muted && LocalRecordingManager.selfRecording.withVideo) {
dispatch(openDialog(StopRecordingDialog, { localRecordingVideoStop: true }));
return;
} else if (!action.muted && !LocalRecordingManager.selfRecording.withVideo) {
dispatch(showNotification({
titleKey: 'recording.localRecordingNoVideo',
descriptionKey: 'recording.localRecordingVideoWarning',
uid: 'recording.localRecordingNoVideo'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}
}
}
}
return next(action);
});

View File

@@ -477,18 +477,19 @@ export function participantMutedUs(participant: any, track: any) {
/**
* Action to create a virtual screenshare participant.
*
* @param {(string)} sourceName - JitsiTrack instance.
* @param {(boolean)} local - JitsiTrack instance.
* @param {(string)} sourceName - The source name of the JitsiTrack instance.
* @param {(boolean)} local - Whether it's a local or remote participant.
* @param {JitsiConference} conference - The conference instance for which the participant is to be created.
* @returns {Function}
*/
export function createVirtualScreenshareParticipant(sourceName: string, local: boolean) {
export function createVirtualScreenshareParticipant(sourceName: string, local: boolean, conference: any) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const ownerId = getVirtualScreenshareParticipantOwnerId(sourceName);
const ownerName = getParticipantDisplayName(state, ownerId);
dispatch(participantJoined({
conference: state['features/base/conference'].conference,
conference,
fakeParticipant: local ? FakeParticipant.LocalScreenShare : FakeParticipant.RemoteScreenShare,
id: sourceName,
name: ownerName

View File

@@ -1,9 +1,7 @@
/* eslint-disable lines-around-comment */
// @ts-ignore
import { getGravatarURL } from '@jitsi/js-utils/avatar';
import { IReduxState, IStore } from '../../app/types';
// @ts-ignore
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
import { IStateful } from '../app/types';
import { GRAVATAR_BASE_URL } from '../avatar/constants';
@@ -20,6 +18,7 @@ import {
PARTICIPANT_ROLE,
WHITEBOARD_PARTICIPANT_ICON
} from './constants';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { preloadImage } from './preloadImage';
import { FakeParticipant, IParticipant } from './types';

View File

@@ -46,7 +46,7 @@ function _updateScreenshareParticipants({ getState, dispatch }: IStore) {
if (getMultipleVideoSendingSupportFeatureFlag(state)) {
if (!localScreenShare && newLocalSceenshareSourceName) {
dispatch(createVirtualScreenshareParticipant(newLocalSceenshareSourceName, true));
dispatch(createVirtualScreenshareParticipant(newLocalSceenshareSourceName, true, conference));
}
if (localScreenShare && !newLocalSceenshareSourceName) {
@@ -68,7 +68,7 @@ function _updateScreenshareParticipants({ getState, dispatch }: IStore) {
}
if (addedScreenshareSourceNames.length) {
addedScreenshareSourceNames.forEach(id => dispatch(createVirtualScreenshareParticipant(id, false)));
addedScreenshareSourceNames.forEach(id => dispatch(
createVirtualScreenshareParticipant(id, false, conference)));
}
}

View File

@@ -13,6 +13,8 @@ export interface IParticipant {
dominantSpeaker?: boolean;
e2eeEnabled?: boolean;
e2eeSupported?: boolean;
e2eeVerificationAvailable?: boolean;
e2eeVerified?: boolean;
email?: string;
fakeParticipant?: FakeParticipant;
features?: {

View File

@@ -10,8 +10,8 @@ import { navigate }
from '../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
// @ts-ignore
import { screen } from '../../../../mobile/navigation/routes';
// @ts-ignore
import { SETTINGS_ENABLED, getFeatureFlag } from '../../../flags';
import { SETTINGS_ENABLED } from '../../../flags/constants';
import { getFeatureFlag } from '../../../flags/functions';
import { connect } from '../../../redux/functions';
/**

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import _ from 'lodash';
import { AnyAction } from 'redux';

View File

@@ -9,6 +9,17 @@
*/
export const SET_NO_SRC_DATA_NOTIFICATION_UID = 'SET_NO_SRC_DATA_NOTIFICATION_UID';
/**
* Sets the track operation promise.
*
* {
* type: SET_TRACK_OPERATIONS_PROMISE,
* audioTrackOperationsPromise: Promise<void>,
* videoTrackOperationsPromise: Promise<void>
* }
*/
export const SET_TRACK_OPERATIONS_PROMISE = 'SET_TRACK_OPERATIONS_PROMISE';
/**
* The type of redux action dispatched when a track has been (locally or
* remotely) added to the conference.

View File

@@ -3,22 +3,22 @@ import { sendAnalytics } from '../../analytics/functions';
import { IStore } from '../../app/types';
import { showErrorNotification, showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT, NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getCurrentConference } from '../conference/functions';
import { IJitsiConference } from '../conference/reducer';
import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any';
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,
MEDIA_TYPE,
MediaType,
SCREENSHARE_MUTISM_AUTHORITY,
VIDEO_MUTISM_AUTHORITY,
VIDEO_TYPE,
VideoType
} from '../media/constants';
import { getLocalParticipant } from '../participants/functions';
import { updateSettings } from '../settings/actions';
import {
SET_NO_SRC_DATA_NOTIFICATION_UID,
@@ -38,7 +38,8 @@ import {
getLocalTrack,
getLocalTracks,
getLocalVideoTrack,
getTrackByJitsiTrack
getTrackByJitsiTrack,
setTrackMuted
} from './functions';
import logger from './logger';
import { ITrackOptions } from './types';
@@ -57,7 +58,7 @@ export function addLocalTrack(newTrack: any) {
await conference.addTrack(newTrack);
}
const setMuted = newTrack.isVideoTrack()
const setMutedA = newTrack.isVideoTrack()
? getMultipleVideoSendingSupportFeatureFlag(getState())
&& newTrack.getVideoType() === VIDEO_TYPE.DESKTOP
? setScreenshareMuted
@@ -66,7 +67,7 @@ export function addLocalTrack(newTrack: any) {
const isMuted = newTrack.isMuted();
logger.log(`Adding ${newTrack.getType()} track - ${isMuted ? 'muted' : 'unmuted'}`);
await dispatch(setMuted(isMuted));
await dispatch(setMutedA(isMuted));
return dispatch(_addTracks([ newTrack ]));
};
@@ -139,6 +140,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
dispatch,
getState
};
const promises: Promise<any>[] = [];
// The following executes on React Native only at the time of this
// writing. The effort to port Web's createInitialLocalTracksAndConnect
@@ -216,7 +218,14 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
mediaType: device
}
});
promises.push(gumProcess.catch(() => {
// ignore the error in the result promises so that the Promise.all resolves after all promises are
// settled.
}));
}
return Promise.all(promises);
};
}
@@ -329,7 +338,7 @@ export function replaceLocalTrack(oldTrack: any, newTrack: any, conference?: IJi
* @param {JitsiLocalTrack|null} newTrack - The track to use instead.
* @returns {Function}
*/
function replaceStoredTracks(oldTrack: any, newTrack: any) {
export function replaceStoredTracks(oldTrack: any, newTrack: any) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
// We call dispose after doing the replace because dispose will
// try and do a new o/a after the track removes itself. Doing it
@@ -345,7 +354,7 @@ function replaceStoredTracks(oldTrack: any, newTrack: any) {
// should be falsey. As such, emit a mute event here to set up the app to reflect the track's mute
// state. If this is not done, the current mute state of the app will be reflected on the track,
// not vice-versa.
const setMuted = newTrack.isVideoTrack()
const setMutedA = newTrack.isVideoTrack()
? getMultipleVideoSendingSupportFeatureFlag(getState())
&& newTrack.getVideoType() === VIDEO_TYPE.DESKTOP
? setScreenshareMuted
@@ -356,7 +365,7 @@ function replaceStoredTracks(oldTrack: any, newTrack: any) {
sendAnalytics(createTrackMutedEvent(newTrack.getType(), 'track.replaced', isMuted));
logger.log(`Replace ${newTrack.getType()} track - ${isMuted ? 'muted' : 'unmuted'}`);
await dispatch(setMuted(isMuted));
await dispatch(setMutedA(isMuted));
await dispatch(_addTracks([ newTrack ]));
}
};
@@ -817,37 +826,48 @@ export function updateLastTrackVideoMediaEvent(track: any, name: string): {
};
}
/**
* Toggles the facingMode constraint on the video stream.
* Mutes or unmutes a local track with a specific media type.
*
* @returns {Function}
* @param {Object} options - Parameters of the function.
* @private
* @returns {Promise}
*/
export function toggleCamera() {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
export function setMuted({ ensureTrack, authority, mediaType, muted }: {
authority?: number; ensureTrack: boolean; mediaType: MediaType; muted: boolean; }) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<any> => {
const state = getState();
const tracks = state['features/base/tracks'];
const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
const currentFacingMode = localVideoTrack.getCameraFacingMode();
const localTrack = getLocalTrack(state['features/base/tracks'], mediaType, true);
/**
* FIXME: Ideally, we should be dispatching {@code replaceLocalTrack} here,
* but it seems to not trigger the re-rendering of the local video on Chrome;
* could be due to a plan B vs unified plan issue. Therefore, we use the legacy
* method defined in conference.js that manually takes care of updating the local
* video as well.
*/
await APP.conference.useVideoStream(null);
if (mediaType === MEDIA_TYPE.SCREENSHARE
&& getMultipleVideoSendingSupportFeatureFlag(state)
&& !muted) {
return Promise.resolve();
}
const targetFacingMode = currentFacingMode === CAMERA_FACING_MODE.USER
? CAMERA_FACING_MODE.ENVIRONMENT
: CAMERA_FACING_MODE.USER;
if (localTrack) {
// The `jitsiTrack` property will have a value only for a localTrack for which `getUserMedia` has
// already completed. If there's no `jitsiTrack`, then the `muted` state will be applied once the
// `jitsiTrack` is created.
const { jitsiTrack } = localTrack;
const isAudioOnly = (mediaType === MEDIA_TYPE.VIDEO && authority === VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY)
|| (mediaType === MEDIA_TYPE.SCREENSHARE && authority === SCREENSHARE_MUTISM_AUTHORITY.AUDIO_ONLY);
// 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 }));
// Screenshare cannot be unmuted using the video mute button unless it is muted by audioOnly in
// the legacy screensharing mode.
if (jitsiTrack && (
jitsiTrack.videoType !== 'desktop' || isAudioOnly || getMultipleVideoSendingSupportFeatureFlag(state))
) {
return setTrackMuted(jitsiTrack, muted, state).catch(
() => dispatch(trackMuteUnmuteFailed(localTrack, muted)));
}
} else if (!muted && ensureTrack && (typeof APP === 'undefined' || isPrejoinPageVisible(state))) {
// FIXME: This only runs on mobile now because web has its own way of
// creating local tracks. Adjust the check once they are unified.
return dispatch(createLocalTracksA({ devices: [ mediaType ] }));
}
const newVideoTrack = await createLocalTrack('video', null, null, { facingMode: targetFacingMode });
// FIXME: See above.
await APP.conference.useVideoStream(newVideoTrack);
return Promise.resolve();
};
}

View File

@@ -1,13 +1,13 @@
/* eslint-disable lines-around-comment */
import { IReduxState, IStore } from '../../app/types';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions';
import { setAudioOnly } from '../audio-only/actions';
import JitsiMeetJS from '../lib-jitsi-meet';
import { destroyLocalDesktopTrackIfExists, replaceLocalTrack } from './actions.any';
import { getLocalVideoTrack, isLocalVideoTrackDesktop } from './functions';
import { getLocalVideoTrack, isLocalVideoTrackDesktop } from './functions.native';
import { TrackOperationType } from './types';
/* eslint-enable lines-around-comment */
export * from './actions.any';
@@ -72,3 +72,22 @@ function _startScreenSharing(dispatch: Function, state: IReduxState) {
setPictureInPictureEnabled(true);
});
}
/**
* Executes a track operation.
*
* NOTE: This is dummy implementation for mobile. Currently we are not sure if we need to chain the track operations.
* For now we are just executing the passed operation without chaining it.
*
* @param {TrackOperationType} type - The type of the operation ('audio', 'video' or 'audio-video').
* @param {Function} operation - The operation.
* @returns {{
* type: SET_TRACK_OPERATIONS_PROMISE,
* audioTrackOperationsPromise: Promise<void>,
* videoTrackOperationsPromise: Promise<void>
* }}
*/
export function executeTrackOperation(type: TrackOperationType, operation: () => Promise<any>) {
return () => operation();
}

View File

@@ -8,7 +8,6 @@ import { showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
// @ts-ignore
import { stopReceiver } from '../../remote-control/actions';
// @ts-ignore
import { setScreenAudioShareState, setScreenshareAudioTrack } from '../../screen-share/actions';
import { isAudioOnlySharing, isScreenVideoShared } from '../../screen-share/functions';
// @ts-ignore
@@ -18,10 +17,13 @@ import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEff
import { setAudioOnly } from '../audio-only/actions';
import { getCurrentConference } from '../conference/functions';
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
import { createLocalTrack } from '../lib-jitsi-meet/functions.any';
import { setScreenshareMuted } from '../media/actions';
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import { CAMERA_FACING_MODE, MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import { updateSettings } from '../settings/actions';
/* eslint-enable lines-around-comment */
import { SET_TRACK_OPERATIONS_PROMISE } from './actionTypes';
import {
addLocalTrack,
replaceLocalTrack
@@ -29,9 +31,10 @@ import {
import {
createLocalTracksF,
getLocalDesktopTrack,
getLocalJitsiAudioTrack
getLocalJitsiAudioTrack,
getLocalVideoTrack
} from './functions';
import { IShareOptions, IToggleScreenSharingOptions } from './types';
import { IShareOptions, IToggleScreenSharingOptions, TrackOperationType } from './types';
export * from './actions.any';
@@ -149,8 +152,6 @@ async function _toggleScreenSharing(
const audioOnlySharing = isAudioOnlySharing(state);
const screenSharing = isScreenVideoShared(state);
const conference = getCurrentConference(state);
const localAudio = getLocalJitsiAudioTrack(state);
const localScreenshare = getLocalDesktopTrack(state['features/base/tracks']);
// Toggle screenshare or audio-only share if the new state is not passed. Happens in the following two cases.
// 1. ShareAudioDialog passes undefined when the user hits continue in the share audio demo modal.
@@ -200,11 +201,16 @@ async function _toggleScreenSharing(
throw new Error(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK);
}
} else if (desktopVideoTrack) {
if (localScreenshare) {
await dispatch(replaceLocalTrack(localScreenshare.jitsiTrack, desktopVideoTrack, conference));
} else {
await dispatch(addLocalTrack(desktopVideoTrack));
}
await dispatch(executeTrackOperation(TrackOperationType.Video, async () => {
const localScreenshare = getLocalDesktopTrack(getState()['features/base/tracks']);
if (localScreenshare) {
await dispatch(replaceLocalTrack(localScreenshare.jitsiTrack, desktopVideoTrack, conference));
} else {
await dispatch(addLocalTrack(desktopVideoTrack));
}
}));
if (isScreenshotCaptureEnabled(state, false, true)) {
dispatch(toggleScreenshotCaptureSummary(true));
}
@@ -217,15 +223,23 @@ async function _toggleScreenSharing(
// Noise suppression doesn't work with desktop audio because we can't chain track effects yet, disable it
// first. We need to to wait for the effect to clear first or it might interfere with the audio mixer.
await dispatch(setNoiseSuppressionEnabled(false));
_maybeApplyAudioMixerEffect(desktopAudioTrack, state);
dispatch(setScreenshareAudioTrack(desktopAudioTrack));
// Handle the case where screen share was stopped from the browsers 'screen share in progress' window.
if (audioOnly) {
desktopAudioTrack?.on(
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => dispatch(toggleScreensharing(undefined, true)));
}
dispatch(executeTrackOperation(TrackOperationType.Audio,
() => {
const result = _maybeApplyAudioMixerEffect(desktopAudioTrack, state);
dispatch(setScreenshareAudioTrack(desktopAudioTrack));
// Handle the case where screen share was stopped from the browsers 'screen share in progress'
// window.
if (audioOnly) {
desktopAudioTrack?.on(
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => dispatch(toggleScreensharing(undefined, true)));
}
return result;
}));
}
// Disable audio-only or best performance mode if the user starts screensharing. This doesn't apply to
@@ -242,16 +256,25 @@ async function _toggleScreenSharing(
dispatch(toggleScreenshotCaptureSummary(false));
// Mute the desktop track instead of removing it from the conference since we don't want the client to signal
// a source-remove to the remote peer for the screenshare track. Later when screenshare is enabled again, the
// same sender will be re-used without the need for signaling a new ssrc through source-add.
dispatch(setScreenshareMuted(true));
await dispatch(executeTrackOperation(TrackOperationType.Video, () => {
// Mute the desktop track instead of removing it from the conference since we don't want the client to
// signal a source-remove to the remote peer for the screenshare track. Later when screenshare is enabled
// again, the same sender will be re-used without the need for signaling a new ssrc through source-add.
dispatch(setScreenshareMuted(true));
return Promise.resolve();
}));
if (desktopAudioTrack) {
if (localAudio) {
localAudio.setEffect(undefined);
} else {
await conference.replaceTrack(desktopAudioTrack, null);
}
await dispatch(executeTrackOperation(TrackOperationType.Audio, async () => {
const localAudio = getLocalJitsiAudioTrack(state);
if (localAudio) {
await localAudio.setEffect(undefined);
} else {
await conference.replaceTrack(desktopAudioTrack, null);
}
}));
desktopAudioTrack.dispose();
dispatch(setScreenshareAudioTrack(null));
}
@@ -264,3 +287,101 @@ async function _toggleScreenSharing(
APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails);
}
}
/**
* Executes a track operation.
*
* @param {TrackOperationType} type - The type of the operation ('audio', 'video' or 'audio-video').
* @param {Function} operation - The operation.
* @returns {{
* type: SET_TRACK_OPERATIONS_PROMISE,
* audioTrackOperationsPromise: Promise<void>,
* videoTrackOperationsPromise: Promise<void>
* }}
*/
export function executeTrackOperation(type: TrackOperationType, operation: () => Promise<any>) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const {
audioTrackOperationsPromise,
videoTrackOperationsPromise
} = getState()['features/base/track-operations'];
switch (type) {
case TrackOperationType.Audio: {
const promise = audioTrackOperationsPromise.then(operation, operation);
dispatch({
type: SET_TRACK_OPERATIONS_PROMISE,
audioTrackOperationsPromise: promise
});
return promise;
}
case TrackOperationType.Video: {
const promise = videoTrackOperationsPromise.then(operation, operation);
dispatch({
type: SET_TRACK_OPERATIONS_PROMISE,
videoTrackOperationsPromise: promise
});
return promise;
}
case TrackOperationType.AudioVideo: {
const promise = Promise.allSettled([
audioTrackOperationsPromise,
videoTrackOperationsPromise
]).then(operation);
dispatch({
type: SET_TRACK_OPERATIONS_PROMISE,
audioTrackOperationsPromise: promise,
videoTrackOperationsPromise: promise
});
return promise;
}
default: {
const unexpectedType: never = type;
return Promise.reject(new Error(`Unexpected track operation type: ${unexpectedType}`));
}
}
};
}
/**
* Toggles the facingMode constraint on the video stream.
*
* @returns {Function}
*/
export function toggleCamera() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) =>
dispatch(executeTrackOperation(TrackOperationType.Video, () =>
/**
* FIXME: Ideally, we should be dispatching {@code replaceLocalTrack} here,
* but it seems to not trigger the re-rendering of the local video on Chrome;
* could be due to a plan B vs unified plan issue. Therefore, we use the legacy
* method defined in conference.js that manually takes care of updating the local
* video as well.
*/
APP.conference.useVideoStream(null).then(() => {
const state = getState();
const tracks = state['features/base/tracks'];
const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
const currentFacingMode = localVideoTrack.getCameraFacingMode();
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 }));
return createLocalTrack('video', null, null, { facingMode: targetFacingMode });
})
.then((newVideoTrack: any) => APP.conference.useVideoStream(newVideoTrack))));
}

View File

@@ -9,10 +9,12 @@ import {
getUserSelectedMicDeviceId
} from '../settings/functions.web';
import { executeTrackOperation } from './actions.web';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import loadEffects from './loadEffects';
import logger from './logger';
import { ITrackOptions } from './types';
import { ITrackOptions, TrackOperationType } from './types';
export * from './functions.any';
@@ -107,14 +109,15 @@ export function createPrejoinTracks() {
const initialDevices = [ 'audio' ];
const requestedAudio = true;
let requestedVideo = false;
const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
const { dispatch, getState } = APP.store;
const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = getState()['features/base/settings'];
// Always get a handle on the audio input device so that we have statistics even if the user joins the
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
// only after that point.
if (startWithAudioMuted) {
APP.store.dispatch(setAudioMuted(true));
dispatch(executeTrackOperation(TrackOperationType.Audio, () => dispatch(setAudioMuted(true))));
}
if (!startWithVideoMuted && !startAudioOnly) {
@@ -128,10 +131,11 @@ export function createPrejoinTracks() {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
firePermissionPromptIsShownEvent: true
}, APP.store)
tryCreateLocalTracks = dispatch(executeTrackOperation(TrackOperationType.AudioVideo, () =>
createLocalTracksF({
devices: initialDevices,
firePermissionPromptIsShownEvent: true
}, APP.store)
.catch((err: Error) => {
if (requestedAudio && requestedVideo) {
@@ -177,7 +181,7 @@ export function createPrejoinTracks() {
errors.videoOnlyError = err;
return [];
});
})));
}
return {

View File

@@ -1,42 +1,17 @@
import { batch } from 'react-redux';
import { IStore } from '../../app/types';
import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getCurrentConference } from '../conference/functions';
import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any';
import {
SET_AUDIO_MUTED,
SET_CAMERA_FACING_MODE,
SET_SCREENSHARE_MUTED,
SET_VIDEO_MUTED,
TOGGLE_CAMERA_FACING_MODE
} from '../media/actionTypes';
import { toggleCameraFacingMode } from '../media/actions';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
MediaType,
SCREENSHARE_MUTISM_AUTHORITY,
VIDEO_MUTISM_AUTHORITY
MediaType
} from '../media/constants';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import {
TRACK_UPDATED
} from './actionTypes';
import {
createLocalTracksA,
destroyLocalTracks,
trackMuteUnmuteFailed,
trackRemoved
} from './actions';
import {
getLocalTrack,
isUserInteractionRequiredForUnmute,
setTrackMuted
} from './functions';
import { TRACK_UPDATED } from './actionTypes';
import { getLocalTrack } from './functions';
import './subscriber';
/**
@@ -49,15 +24,6 @@ import './subscriber';
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SET_AUDIO_MUTED:
if (!action.muted
&& isUserInteractionRequiredForUnmute(store.getState())) {
return;
}
_setMuted(store, action, MEDIA_TYPE.AUDIO);
break;
case SET_CAMERA_FACING_MODE: {
// XXX The camera facing mode of a MediaStreamTrack can be specified
// only at initialization time and then it can only be toggled. So in
@@ -78,19 +44,6 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case SET_SCREENSHARE_MUTED:
_setMuted(store, action, action.mediaType);
break;
case SET_VIDEO_MUTED:
if (!action.muted
&& isUserInteractionRequiredForUnmute(store.getState())) {
return;
}
_setMuted(store, action, action.mediaType);
break;
case TOGGLE_CAMERA_FACING_MODE: {
const localTrack = _getLocalTrack(store, MEDIA_TYPE.VIDEO);
let jitsiTrack;
@@ -121,31 +74,6 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
/**
* Set up state change listener to perform maintenance tasks when the conference
* is left or failed, remove all tracks from the store.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch, getState }, prevConference) => {
const { authRequired, error } = getState()['features/base/conference'];
// conference keep flipping while we are authenticating, skip clearing while we are in that process
if (prevConference && !conference && !authRequired && !error) {
// Clear all tracks.
const remoteTracks = getState()['features/base/tracks'].filter(t => !t.local);
batch(() => {
dispatch(destroyLocalTracks());
for (const track of remoteTracks) {
dispatch(trackRemoved(track.jitsiTrack));
}
dispatch({ type: _RESET_BREAKOUT_ROOMS });
});
}
});
/**
* Gets the local track associated with a specific {@code MEDIA_TYPE} in a
* specific redux store.
@@ -173,48 +101,3 @@ function _getLocalTrack(
mediaType,
includePending));
}
/**
* Mutes or unmutes a local track with a specific media type.
*
* @param {Store} store - The redux store in which the specified action is
* dispatched.
* @param {Action} action - The redux action dispatched in the specified store.
* @param {MEDIA_TYPE} mediaType - The {@link MEDIA_TYPE} of the local track
* which is being muted or unmuted.
* @private
* @returns {void}
*/
async function _setMuted(store: IStore, { ensureTrack, authority, muted }: {
authority: number; ensureTrack: boolean; muted: boolean; }, mediaType: MediaType) {
const { dispatch, getState } = store;
const localTrack = _getLocalTrack(store, mediaType, /* includePending */ true);
const state = getState();
if (mediaType === MEDIA_TYPE.SCREENSHARE
&& getMultipleVideoSendingSupportFeatureFlag(state)
&& !muted) {
return;
}
if (localTrack) {
// The `jitsiTrack` property will have a value only for a localTrack for which `getUserMedia` has already
// completed. If there's no `jitsiTrack`, then the `muted` state will be applied once the `jitsiTrack` is
// created.
const { jitsiTrack } = localTrack;
const isAudioOnly = (mediaType === MEDIA_TYPE.VIDEO && authority === VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY)
|| (mediaType === MEDIA_TYPE.SCREENSHARE && authority === SCREENSHARE_MUTISM_AUTHORITY.AUDIO_ONLY);
// Screenshare cannot be unmuted using the video mute button unless it is muted by audioOnly in the legacy
// screensharing mode.
if (jitsiTrack && (
jitsiTrack.videoType !== 'desktop' || isAudioOnly || getMultipleVideoSendingSupportFeatureFlag(state))
) {
setTrackMuted(jitsiTrack, muted, state).catch(() => dispatch(trackMuteUnmuteFailed(localTrack, muted)));
}
} else if (!muted && ensureTrack && (typeof APP === 'undefined' || isPrejoinPageVisible(state))) {
// FIXME: This only runs on mobile now because web has its own way of
// creating local tracks. Adjust the check once they are unified.
dispatch(createLocalTracksA({ devices: [ mediaType ] }));
}
}

View File

@@ -18,6 +18,7 @@ import {
TRACK_UPDATED
} from './actionTypes';
import {
executeTrackOperation,
showNoDataFromSourceVideoError,
toggleScreensharing,
trackNoDataFromSourceNotificationInfoChanged
@@ -25,7 +26,7 @@ import {
import {
getTrackByJitsiTrack
} from './functions.web';
import { ITrack } from './types';
import { ITrack, TrackOperationType } from './types';
import './middleware.any';
@@ -67,9 +68,10 @@ MiddlewareRegistry.register(store => next => action => {
const { jitsiTrack } = action.track;
const muted = action.wasMuted;
const isVideoTrack = jitsiTrack.getType() !== MEDIA_TYPE.AUDIO;
const { dispatch } = store;
if (isVideoTrack && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) {
store.dispatch(setScreenshareMuted(!muted));
dispatch(executeTrackOperation(TrackOperationType.Video, () => dispatch(setScreenshareMuted(!muted))));
} else if (isVideoTrack) {
APP.conference.setVideoMuteStatus();
} else {

View File

@@ -4,6 +4,7 @@ import { set } from '../redux/functions';
import {
SET_NO_SRC_DATA_NOTIFICATION_UID,
SET_TRACK_OPERATIONS_PROMISE,
TRACK_ADDED,
TRACK_CREATE_CANCELED,
TRACK_CREATE_ERROR,
@@ -152,3 +153,31 @@ ReducerRegistry.register<INoSrcDataState>('features/base/no-src-data', (state =
}
});
export interface ITrackOperations {
audioTrackOperationsPromise: Promise<void>;
videoTrackOperationsPromise: Promise<void>;
}
const DEFAULT_TRACK_OPERATIONS_STATE = {
audioTrackOperationsPromise: Promise.resolve(),
videoTrackOperationsPromise: Promise.resolve()
};
/**
* Listen for actions that mutate the no-src-data state, like the current notification id.
*/
ReducerRegistry.register<ITrackOperations>(
'features/base/track-operations',
(state = DEFAULT_TRACK_OPERATIONS_STATE, action): ITrackOperations => {
switch (action.type) {
case SET_TRACK_OPERATIONS_PROMISE:
return {
...state,
audioTrackOperationsPromise: action.audioTrackOperationsPromise || state.audioTrackOperationsPromise,
videoTrackOperationsPromise: action.videoTrackOperationsPromise || state.videoTrackOperationsPromise
};
default:
return state;
}
});

View File

@@ -1,9 +1,13 @@
import _ from 'lodash';
import { batch } from 'react-redux';
import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
import { getCurrentConference } from '../conference/functions';
import { MEDIA_TYPE } from '../media/constants';
import { getScreenshareParticipantIds } from '../participants/functions';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { destroyLocalTracks, trackRemoved } from './actions.any';
import { isLocalTrackMuted } from './functions';
/**
@@ -38,3 +42,28 @@ StateListenerRegistry.register(
}
}
);
/**
* Set up state change listener to perform maintenance tasks when the conference
* is left or failed, remove all tracks from the store.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch, getState }, prevConference) => {
const { authRequired, error } = getState()['features/base/conference'];
// conference keep flipping while we are authenticating, skip clearing while we are in that process
if (prevConference && !conference && !authRequired && !error) {
// Clear all tracks.
const remoteTracks = getState()['features/base/tracks'].filter(t => !t.local);
batch(() => {
dispatch(destroyLocalTracks());
for (const track of remoteTracks) {
dispatch(trackRemoved(track.jitsiTrack));
}
dispatch({ type: _RESET_BREAKOUT_ROOMS });
});
}
});

View File

@@ -72,3 +72,9 @@ export interface IShareOptions {
desktopSharingSources?: string[];
desktopStream?: any;
}
export enum TrackOperationType {
Audio = 'audio',
AudioVideo = 'audio-video',
Video = 'video'
}

View File

@@ -1,4 +1,3 @@
// @ts-ignore
import React from 'react';
import { useTranslation } from 'react-i18next';
import {

View File

@@ -1,8 +1,8 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { TouchableRipple } from 'react-native-paper';
import Icon from '../../../icons/components/Icon';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import styles from '../../../react/components/native/styles';
import { IIconButtonProps } from '../../../react/types';

View File

@@ -27,6 +27,7 @@ const useStyles = makeStyles()(theme => {
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
zIndex: 301,
animation: `${keyframes`
0% {
opacity: 0.4;

View File

@@ -3,6 +3,10 @@ import React, { Component, ComponentType } from 'react';
import { IReduxState } from '../../../../app/types';
import { IReactionEmojiProps } from '../../../../reactions/constants';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { JitsiPortal } from '../../../../toolbox/components/web';
import { showOverflowDrawer } from '../../../../toolbox/functions.web';
import { connect } from '../../../redux/functions';
import DialogTransition from './DialogTransition';
@@ -24,6 +28,11 @@ interface IProps {
*/
_isNewDialog: boolean;
/**
* Whether the overflow drawer should be used.
*/
_overflowDrawer: boolean;
/**
* Array of reactions to be displayed.
*/
@@ -69,7 +78,9 @@ class DialogContainer extends Component<IProps> {
render() {
return this.props._isNewDialog ? (
<DialogTransition>
{this._renderDialogContent()}
{this.props._overflowDrawer
? <JitsiPortal>{this._renderDialogContent()}</JitsiPortal>
: this._renderDialogContent() }
</DialogTransition>
) : (
<ModalTransition>
@@ -90,11 +101,13 @@ class DialogContainer extends Component<IProps> {
function mapStateToProps(state: IReduxState) {
const stateFeaturesBaseDialog = state['features/base/dialog'];
const { reducedUI } = state['features/base/responsive-ui'];
const overflowDrawer = showOverflowDrawer(state);
return {
_component: stateFeaturesBaseDialog.component,
_componentProps: stateFeaturesBaseDialog.componentProps,
_isNewDialog: stateFeaturesBaseDialog.isNewDialog,
_overflowDrawer: overflowDrawer,
_reducedUI: reducedUI
};
}

View File

@@ -1,5 +1,3 @@
// @flow
import {
REFRESH_CALENDAR,
SET_CALENDAR_AUTHORIZATION,
@@ -19,8 +17,7 @@ import {
* isInteractive: boolean
* }}
*/
export function refreshCalendar(
forcePermission: boolean = false, isInteractive: boolean = true) {
export function refreshCalendar(forcePermission = false, isInteractive = true) {
return {
type: REFRESH_CALENDAR,
forcePermission,
@@ -39,7 +36,7 @@ export function refreshCalendar(
* authorization: ?string
* }}
*/
export function setCalendarAuthorization(authorization: ?string) {
export function setCalendarAuthorization(authorization?: string) {
return {
type: SET_CALENDAR_AUTHORIZATION,
authorization

View File

@@ -1,13 +1,15 @@
// @flow
// @ts-expect-error
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
import type { Dispatch } from 'redux';
import { getDefaultURL } from '../app/functions';
import { openDialog } from '../base/dialog';
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { refreshCalendar } from './actions';
import {
UpdateCalendarEventDialog
// @ts-ignore
} from './components';
import { addLinkToCalendarEntry } from './functions.native';
@@ -35,11 +37,13 @@ export function openUpdateCalendarEventDialog(eventId: string) {
* @returns {Function}
*/
export function updateCalendarEvent(eventId: string) {
return (dispatch: Dispatch<any>, getState: Function) => {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const defaultUrl = getDefaultURL(getState);
const roomName = generateRoomWithoutSeparator();
addLinkToCalendarEntry(getState(), eventId, `${defaultUrl}/${roomName}`)
// @ts-ignore
.finally(() => {
dispatch(refreshCalendar(false, false));
});

View File

@@ -1,9 +1,11 @@
// @flow
// @ts-expect-error
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
import type { Dispatch } from 'redux';
import { createCalendarConnectedEvent, sendAnalytics } from '../analytics';
import { createCalendarConnectedEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { loadGoogleAPI } from '../google-api';
import {
@@ -14,8 +16,8 @@ import {
SET_CALENDAR_PROFILE_EMAIL,
SET_LOADING_CALENDAR_EVENTS
} from './actionTypes';
import { refreshCalendar, setCalendarEvents } from './actions';
import { _getCalendarIntegration, isCalendarEnabled } from './functions';
import { refreshCalendar, setCalendarEvents } from './actions.web';
import { _getCalendarIntegration, isCalendarEnabled } from './functions.web';
import logger from './logger';
export * from './actions.any';
@@ -26,8 +28,8 @@ export * from './actions.any';
*
* @returns {Function}
*/
export function bootstrapCalendarIntegration(): Function {
return (dispatch, getState) => {
export function bootstrapCalendarIntegration() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
if (!isCalendarEnabled(state)) {
@@ -63,7 +65,7 @@ export function bootstrapCalendarIntegration(): Function {
}
return dispatch(integrationToLoad._isSignedIn())
.then(signedIn => {
.then((signedIn: boolean) => {
if (signedIn) {
dispatch(setIntegrationReady(integrationType));
dispatch(updateProfile(integrationType));
@@ -114,7 +116,7 @@ export function openUpdateCalendarEventDialog(
* msAuthState: Object
* }}
*/
export function setCalendarAPIAuthState(newState: ?Object) {
export function setCalendarAPIAuthState(newState?: Object) {
return {
type: SET_CALENDAR_AUTH_STATE,
msAuthState: newState
@@ -130,7 +132,7 @@ export function setCalendarAPIAuthState(newState: ?Object) {
* error: Object
* }}
*/
export function setCalendarError(error: ?Object) {
export function setCalendarError(error?: Object) {
return {
type: SET_CALENDAR_ERROR,
error
@@ -146,7 +148,7 @@ export function setCalendarError(error: ?Object) {
* email: string
* }}
*/
export function setCalendarProfileEmail(newEmail: ?string) {
export function setCalendarProfileEmail(newEmail?: string) {
return {
type: SET_CALENDAR_PROFILE_EMAIL,
email: newEmail
@@ -196,8 +198,8 @@ export function setIntegrationReady(integrationType: string) {
* signed into.
* @returns {Function}
*/
export function signIn(calendarType: string): Function {
return (dispatch: Dispatch<any>) => {
export function signIn(calendarType: string) {
return (dispatch: IStore['dispatch']) => {
const integration = _getCalendarIntegration(calendarType);
if (!integration) {
@@ -210,7 +212,7 @@ export function signIn(calendarType: string): Function {
.then(() => dispatch(updateProfile(calendarType)))
.then(() => dispatch(refreshCalendar()))
.then(() => sendAnalytics(createCalendarConnectedEvent()))
.catch(error => {
.catch((error: any) => {
logger.error(
'Error occurred while signing into calendar integration',
error);
@@ -228,10 +230,10 @@ export function signIn(calendarType: string): Function {
* @param {string} calendarId - The id of the calendar to use.
* @returns {Function}
*/
export function updateCalendarEvent(id: string, calendarId: string): Function {
return (dispatch: Dispatch<any>, getState: Function) => {
export function updateCalendarEvent(id: string, calendarId: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { integrationType } = getState()['features/calendar-sync'];
const { integrationType = '' } = getState()['features/calendar-sync'];
const integration = _getCalendarIntegration(integrationType);
if (!integration) {
@@ -240,7 +242,7 @@ export function updateCalendarEvent(id: string, calendarId: string): Function {
const { locationURL } = getState()['features/base/connection'];
const newRoomName = generateRoomWithoutSeparator();
let href = locationURL.href;
let href = locationURL?.href ?? '';
href.endsWith('/') || (href += '/');
@@ -275,8 +277,8 @@ export function updateCalendarEvent(id: string, calendarId: string): Function {
* should be updated.
* @returns {Function}
*/
export function updateProfile(calendarType: string): Function {
return (dispatch: Dispatch<any>) => {
export function updateProfile(calendarType: string) {
return (dispatch: IStore['dispatch']) => {
const integration = _getCalendarIntegration(calendarType);
if (!integration) {
@@ -284,7 +286,7 @@ export function updateProfile(calendarType: string): Function {
}
return dispatch(integration.getCurrentEmail())
.then(email => {
.then((email: string) => {
dispatch(setCalendarProfileEmail(email));
});
};

View File

@@ -1,8 +1,6 @@
// @flow
import md5 from 'js-md5';
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
import { APP_LINK_SCHEME, parseURIString } from '../base/util/uri';
import { setCalendarEvents } from './actions';
import { MAX_LIST_LENGTH } from './constants';
@@ -16,7 +14,8 @@ const ALLDAY_EVENT_LENGTH = 23 * 60 * 60 * 1000;
* @param {Object} entry - The calendar entry.
* @returns {boolean}
*/
function _isDisplayableCalendarEntry(entry) {
function _isDisplayableCalendarEntry(entry: { allDay: boolean; attendees: Object[];
endDate: number; startDate: number; }) {
// Entries are displayable if:
// - Ends in the future (future or ongoing events)
// - Is not an all day event and there is only one attendee (these events
@@ -45,7 +44,8 @@ export function _updateCalendarEntries(events: Array<Object>) {
return;
}
// eslint-disable-next-line no-invalid-this
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-invalid-this
const { dispatch, getState } = this;
const knownDomains = getState()['features/base/known-domains'];
const entryMap = new Map();
@@ -106,7 +106,7 @@ export function _updateCalendarEntries(events: Array<Object>) {
* @param {string} negativePattern - The negative pattern.
* @returns {string}
*/
function _checkPattern(str, positivePattern, negativePattern) {
function _checkPattern(str: string, positivePattern: string, negativePattern: string) {
const positiveRegExp = new RegExp(positivePattern, 'gi');
let positiveMatch = positiveRegExp.exec(str);
@@ -129,7 +129,7 @@ function _checkPattern(str, positivePattern, negativePattern) {
* @private
* @returns {CalendarEntry}
*/
function _parseCalendarEntry(event, knownDomains) {
function _parseCalendarEntry(event: any, knownDomains: string[]) {
if (event) {
const url = _getURLFromEvent(event, knownDomains);
const startDate = Date.parse(event.startDate);
@@ -170,7 +170,8 @@ function _parseCalendarEntry(event, knownDomains) {
* @private
* @returns {string}
*/
function _getURLFromEvent(event, knownDomains) {
function _getURLFromEvent(event: { description: string; location: string; notes: string; title: string;
url: string; }, knownDomains: string[]) {
const linkTerminatorPattern = '[^\\s<>$]';
const urlRegExp
= `http(s)?://(${knownDomains.join('|')})/${linkTerminatorPattern}+`;

View File

@@ -1,15 +1,17 @@
// @flow
import { NativeModules, Platform } from 'react-native';
import RNCalendarEvents from 'react-native-calendar-events';
import type { Store } from 'redux';
import { CALENDAR_ENABLED, getFeatureFlag } from '../base/flags';
import { IReduxState, IStore } from '../app/types';
import { IStateful } from '../base/app/types';
import { CALENDAR_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { getShareInfoText } from '../invite';
import { setCalendarAuthorization } from './actions';
import { setCalendarAuthorization } from './actions.native';
import { FETCH_END_DAYS, FETCH_START_DAYS } from './constants';
import { _updateCalendarEntries } from './functions';
import { _updateCalendarEntries } from './functions.native';
import logger from './logger';
export * from './functions.any';
@@ -23,10 +25,10 @@ export * from './functions.any';
* @returns {Promise<*>}
*/
export function addLinkToCalendarEntry(
state: Object, id: string, link: string): Promise<any> {
state: IReduxState, id: string, link: string): Promise<any> {
return new Promise((resolve, reject) => {
getShareInfoText(state, link, true).then(shareInfoText => {
RNCalendarEvents.findEventById(id).then(event => {
getShareInfoText(state, link, true).then((shareInfoText: string) => {
RNCalendarEvents.findEventById(id).then((event: any) => {
const updateText
= event.description
? `${event.description}\n\n${shareInfoText}`
@@ -43,6 +45,7 @@ export function addLinkToCalendarEntry(
})
};
// @ts-ignore
RNCalendarEvents.saveEvent(event.title, updateObject)
.then(resolve, reject);
}, reject);
@@ -61,7 +64,7 @@ export function addLinkToCalendarEntry(
* @returns {boolean} If the app has enabled the calendar feature, {@code true};
* otherwise, {@code false}.
*/
export function isCalendarEnabled(stateful: Function | Object) {
export function isCalendarEnabled(stateful: IStateful) {
const flag = getFeatureFlag(stateful, CALENDAR_ENABLED);
if (typeof flag !== 'undefined') {
@@ -85,9 +88,9 @@ export function isCalendarEnabled(stateful: Function | Object) {
* @returns {void}
*/
export function _fetchCalendarEntries(
store: Store<*, *>,
store: IStore,
maybePromptForPermission: boolean,
forcePermission: ?boolean) {
forcePermission?: boolean) {
const { dispatch, getState } = store;
const promptForPermission
= (maybePromptForPermission
@@ -104,6 +107,8 @@ export function _fetchCalendarEntries(
endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
RNCalendarEvents.fetchAllEvents(
// @ts-ignore
startDate.getTime(),
endDate.getTime(),
[])
@@ -126,7 +131,7 @@ export function _fetchCalendarEntries(
* @private
* @returns {Promise}
*/
function _ensureCalendarAccess(promptForPermission, dispatch) {
function _ensureCalendarAccess(promptForPermission: boolean | undefined, dispatch: IStore['dispatch']) {
return new Promise((resolve, reject) => {
RNCalendarEvents.checkPermissions()
.then(status => {

View File

@@ -1,12 +1,13 @@
// @flow
import { toState } from '../base/redux';
/* eslint-disable lines-around-comment */
import { IStore } from '../app/types';
import { IStateful } from '../base/app/types';
import { toState } from '../base/redux/functions';
import {
clearCalendarIntegration,
setCalendarError,
setLoadingCalendarEvents
} from './actions';
} from './actions.web';
export * from './functions.any';
import {
CALENDAR_TYPE,
@@ -14,10 +15,13 @@ import {
FETCH_END_DAYS,
FETCH_START_DAYS
} from './constants';
import { _updateCalendarEntries } from './functions';
import { _updateCalendarEntries } from './functions.web';
import logger from './logger';
// @ts-ignore
import { googleCalendarApi } from './web/googleCalendar';
// @ts-ignore
import { microsoftCalendarApi } from './web/microsoftCalendar';
/* eslint-enable lines-around-comment */
/**
* Determines whether the calendar feature is enabled by the web.
@@ -27,7 +31,7 @@ import { microsoftCalendarApi } from './web/microsoftCalendar';
* @returns {boolean} If the app has enabled the calendar feature, {@code true};
* otherwise, {@code false}.
*/
export function isCalendarEnabled(stateful: Function | Object) {
export function isCalendarEnabled(stateful: IStateful) {
const {
enableCalendarIntegration,
googleApiApplicationClientID,
@@ -37,26 +41,24 @@ export function isCalendarEnabled(stateful: Function | Object) {
return Boolean(enableCalendarIntegration && (googleApiApplicationClientID || microsoftApiApplicationClientID));
}
/* eslint-disable no-unused-vars */
/**
* Reads the user's calendar and updates the stored entries if need be.
*
* @param {Object} store - The redux store.
* @param {boolean} maybePromptForPermission - Flag to tell the app if it should
* @param {boolean} _maybePromptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @param {boolean|undefined} forcePermission - Whether to force to re-ask for
* @param {boolean|undefined} _forcePermission - Whether to force to re-ask for
* the permission or not.
* @private
* @returns {void}
*/
export function _fetchCalendarEntries(
store: Object,
maybePromptForPermission: boolean,
forcePermission: ?boolean) {
/* eslint-enable no-unused-vars */
store: IStore,
_maybePromptForPermission: boolean,
_forcePermission?: boolean) {
const { dispatch, getState } = store;
const { integrationType } = getState()['features/calendar-sync'];
const { integrationType = '' } = getState()['features/calendar-sync'];
const integration = _getCalendarIntegration(integrationType);
if (!integration) {
@@ -69,7 +71,7 @@ export function _fetchCalendarEntries(
dispatch(integration.load())
.then(() => dispatch(integration._isSignedIn()))
.then(signedIn => {
.then((signedIn: boolean) => {
if (signedIn) {
return Promise.resolve();
}
@@ -80,13 +82,13 @@ export function _fetchCalendarEntries(
})
.then(() => dispatch(integration.getCalendarEntries(
FETCH_START_DAYS, FETCH_END_DAYS)))
.then(events => _updateCalendarEntries.call({
.then((events: Object[]) => _updateCalendarEntries.call({
dispatch,
getState
}, events))
.then(() => {
dispatch(setCalendarError());
}, error => {
}, (error: any) => {
logger.error('Error fetching calendar.', error);
if (error.error === ERRORS.AUTH_FAILED) {

View File

@@ -1,5 +1,3 @@
// @flow
import { getLogger } from '../base/logging/functions';
export default getLogger('features/calendar-sync');

View File

@@ -1,8 +1,8 @@
// @flow
import { SET_CONFIG } from '../base/config';
import { ADD_KNOWN_DOMAINS } from '../base/known-domains';
import { MiddlewareRegistry, equals } from '../base/redux';
import { IStore } from '../app/types';
import { SET_CONFIG } from '../base/config/actionTypes';
import { ADD_KNOWN_DOMAINS } from '../base/known-domains/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { equals } from '../base/redux/functions';
import { APP_STATE_CHANGED } from '../mobile/background/actionTypes';
import { REFRESH_CALENDAR } from './actionTypes';
@@ -70,7 +70,7 @@ MiddlewareRegistry.register(store => next => action => {
* @private
* @returns {void}
*/
function _maybeClearAccessStatus(store, { appState }) {
function _maybeClearAccessStatus(store: IStore, { appState }: { appState: string; }) {
appState === 'background'
&& store.dispatch(setCalendarAuthorization(undefined));
}

View File

@@ -29,7 +29,11 @@ const DEFAULT_STATE = {
export interface ICalendarSyncState {
authorization?: string;
error?: Object;
events: Array<Object>;
events: Array<{
calendarId: string;
id: string;
url: string;
}>;
integrationReady: boolean;
integrationType?: string;
isLoadingEvents?: boolean;

View File

@@ -6,8 +6,7 @@ import type { Dispatch } from 'redux';
import { v4 as uuidV4 } from 'uuid';
import { findWindows } from 'windows-iana';
import { createDeferred } from '../../../../modules/util/helpers';
import { parseStandardURIString, parseURLParams } from '../../base/util';
import { createDeferred, parseStandardURIString, parseURLParams } from '../../base/util';
import { getShareInfoText } from '../../invite';
import { setCalendarAPIAuthState } from '../actions';

View File

@@ -47,6 +47,7 @@ import {
} from './constants';
import { getUnreadCount } from './functions';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
/* eslint-enable lines-around-comment */
/**
* Timeout for when to show the privacy notice after a private message was received.

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
@@ -9,6 +8,7 @@ import Button from '../../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../../base/ui/constants.native';
import EndMeetingIcon from './EndMeetingIcon';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import styles from './styles';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback, useState } from 'react';
import { TouchableOpacity, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
@@ -17,7 +16,6 @@ import { IconMic, IconMicSlash } from '../../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../../base/media/constants';
import { isLocalTrackMuted } from '../../../../base/tracks/functions';
import { isAudioMuteButtonDisabled } from '../../../../toolbox/functions.any';
// @ts-ignore
import { muteLocal } from '../../../../video-menu/actions';
// @ts-ignore

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -6,9 +5,9 @@ import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IconArrowDown } from '../../../base/icons/svg/index';
import Label from '../../../base/label/components/web/Label';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { Tooltip } from '../../../base/tooltip';
// @ts-ignore
import { setTopPanelVisible } from '../../../filmstrip/actions.web';
const ToggleTopPanelLabel = () => {

View File

@@ -1,6 +1,5 @@
/* eslint-disable lines-around-comment */
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { INDICATOR_DISPLAY_THRESHOLD } from '../AbstractConnectionIndicator';

View File

@@ -1,5 +1,3 @@
/* eslint-disable lines-around-comment */
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import clsx from 'clsx';
@@ -29,11 +27,13 @@ import AbstractConnectionIndicator, {
type State as AbstractState,
INDICATOR_DISPLAY_THRESHOLD,
mapStateToProps as _abstractMapStateToProps
// @ts-ignore
} from '../AbstractConnectionIndicator';
// @ts-ignore
import ConnectionIndicatorContent from './ConnectionIndicatorContent';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { ConnectionIndicatorIcon } from './ConnectionIndicatorIcon';
@@ -228,6 +228,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
<Popover
className = { clsx(classes.container, visibilityClass) }
content = { <ConnectionIndicatorContent
// @ts-ignore
inheritedStats = { this.state.stats }
participantId = { participantId } /> }
@@ -236,6 +237,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
onPopoverClose = { this._onHidePopover }
onPopoverOpen = { this._onShowPopover }
position = { statsPopoverPosition }
// @ts-ignore
visible = { this.state.popoverVisible }>
{ this._renderIndicator() }
@@ -259,6 +261,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted,
_connectionIndicatorInactiveDisabled
// @ts-ignore
} = this.props;
@@ -305,6 +308,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
// @ts-ignore
return this.state.showIndicator
// @ts-ignore
|| this.props.alwaysVisible
|| _isConnectionStatusInterrupted
@@ -348,6 +352,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
_videoTrack,
classes,
iconSize
// @ts-ignore
} = this.props;
@@ -399,5 +404,6 @@ export function _mapStateToProps(state: IReduxState, ownProps: Props) {
}
export default translate(connect(_mapStateToProps)(
// @ts-ignore
withStyles(styles)(ConnectionIndicator)));

View File

@@ -189,6 +189,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
codec = { codec }
connectionSummary = { this._getConnectionStatusTip() }
disableShowMoreStats = { this.props._disableShowMoreStats }
e2eeVerified = { this.props._isE2EEVerified }
enableSaveLogs = { this.props._enableSaveLogs }
framerate = { framerate }
isLocalVideo = { this.props._isLocalVideo }
@@ -328,6 +329,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
_disableShowMoreStats: state['features/base/config'].disableShowMoreStats,
_isConnectionStatusInactive,
_isConnectionStatusInterrupted,
_isE2EEVerified: participant?.e2eeVerified,
_isVirtualScreenshareParticipant: isScreenShareParticipant(participant),
_isLocalVideo: participant?.local,
_region: participant?.region,

View File

@@ -73,6 +73,11 @@ interface IProps extends WithTranslation {
*/
disableShowMoreStats: boolean;
/**
* Whether or not the participant was verified.
*/
e2eeVerified: boolean;
/**
* Whether or not should display the "Save Logs" link.
*/
@@ -486,6 +491,31 @@ class ConnectionStatsTable extends Component<IProps> {
);
}
/**
* Creates a a table row as a ReactElement for displaying e2ee verication status, if present.
*
* @private
* @returns {ReactElement}
*/
_renderE2EEVerified() {
const { e2eeVerified, t } = this.props;
if (e2eeVerified === undefined) {
return;
}
const status = e2eeVerified ? '\u{2705}' : '\u{274C}';
return (
<tr>
<td>
<span>{ t('connectionindicator.e2eeVerified') }</span>
</td>
<td>{ status }</td>
</tr>
);
}
/**
* Creates a table row as a ReactElement for displaying a summary message
@@ -726,6 +756,7 @@ class ConnectionStatsTable extends Component<IProps> {
{ this._renderResolution() }
{ this._renderFrameRate() }
{ this._renderCodecs() }
{ this._renderE2EEVerified() }
</tbody>
</table>
);

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import Tabs from '@atlaskit/tabs';
import React, { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
@@ -8,6 +7,7 @@ import { hideDialog } from '../../base/dialog/actions';
import { translate } from '../../base/i18n/functions';
import { connect } from '../../base/redux/functions';
import Dialog from '../../base/ui/components/web/Dialog';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { obtainDesktopSources } from '../functions';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -11,9 +10,9 @@ import {
} from '../../../base/participants/functions';
import { updateSettings } from '../../../base/settings/actions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { Tooltip } from '../../../base/tooltip';
// @ts-ignore
import { getIndicatorsTooltipPosition } from '../../../filmstrip/functions.web';
import { appendSuffix } from '../../functions';

View File

@@ -1,5 +1,3 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@@ -12,11 +10,9 @@ import {
isWhiteboardParticipant
} from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
// @ts-ignore
import { getLargeVideoParticipant } from '../../../large-video/functions';
import { isToolboxVisible } from '../../../toolbox/functions.web';
// @ts-ignore
import { isLayoutTileView } from '../../../video-layout';
import { isLayoutTileView } from '../../../video-layout/functions.web';
import DisplayNameBadge from './DisplayNameBadge';

View File

@@ -43,7 +43,7 @@ export function authorizeDropbox() {
* expireDate: number
* }}
*/
export function updateDropboxToken(token: string, rToken: string, expireDate: number) {
export function updateDropboxToken(token?: string, rToken?: string, expireDate?: number) {
return {
type: UPDATE_DROPBOX_TOKEN,
token,

View File

@@ -43,3 +43,7 @@ export const SET_MAX_MODE = 'SET_MAX_MODE';
* }
*/
export const SET_MEDIA_ENCRYPTION_KEY = 'SET_MEDIA_ENCRYPTION_KEY';
export const START_VERIFICATION = 'START_VERIFICATION';
export const PARTICIPANT_VERIFIED = 'PARTICIPANT_VERIFIED';

View File

@@ -1,8 +1,10 @@
import {
PARTICIPANT_VERIFIED,
SET_EVERYONE_ENABLED_E2EE,
SET_EVERYONE_SUPPORT_E2EE,
SET_MAX_MODE,
SET_MEDIA_ENCRYPTION_KEY,
START_VERIFICATION,
TOGGLE_E2EE } from './actionTypes';
/**
@@ -80,3 +82,38 @@ export function setMediaEncryptionKey(keyInfo: Object) {
keyInfo
};
}
/**
* Dispatches an action to start participant e2ee verficiation process.
*
* @param {string} pId - The participant id.
* @returns {{
* type: START_VERIFICATION,
* pId: string
* }}
*/
export function startVerification(pId: string) {
return {
type: START_VERIFICATION,
pId
};
}
/**
* Dispatches an action to set participant e2ee verification status.
*
* @param {string} pId - The participant id.
* @param {boolean} isVerified - The verifcation status.
* @returns {{
* type: PARTICIPANT_VERIFIED,
* pId: string,
* isVerified: boolean
* }}
*/
export function participantVerified(pId: string, isVerified: boolean) {
return {
type: PARTICIPANT_VERIFIED,
pId,
isVerified
};
}

View File

@@ -0,0 +1,166 @@
import { withStyles } from '@mui/styles';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../app/types';
import { translate } from '../../base/i18n/functions';
import { getParticipantById } from '../../base/participants/functions';
import { connect } from '../../base/redux/functions';
import Dialog from '../../base/ui/components/web/Dialog';
import { participantVerified } from '../actions';
import { ISas } from '../reducer';
interface IProps extends WithTranslation {
classes: any;
decimal: string;
dispatch: IStore['dispatch'];
emoji: string;
pId: string;
participantName: string;
sas: ISas;
}
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current UI theme.
*
* @returns {Object}
*/
const styles = () => {
return {
container: {
display: 'flex',
flexDirection: 'column',
margin: '16px'
},
row: {
alignSelf: 'center',
display: 'flex'
},
item: {
textAlign: 'center',
margin: '16px'
},
emoji: {
fontSize: '40px',
margin: '12px'
}
};
};
/**
* Class for the dialog displayed for E2EE sas verification.
*/
export class ParticipantVerificationDialog extends Component<IProps> {
/**
* Instantiates a new instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onConfirmed = this._onConfirmed.bind(this);
this._onDismissed = this._onDismissed.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { emoji } = this.props.sas;
const { participantName } = this.props;
const { classes, t } = this.props;
return (
<Dialog
cancel = {{ translationKey: 'dialog.verifyParticipantDismiss' }}
ok = {{ translationKey: 'dialog.verifyParticipantConfirm' }}
onCancel = { this._onDismissed }
onSubmit = { this._onConfirmed }
titleKey = 'dialog.verifyParticipantTitle'>
<div>
{ t('dialog.verifyParticipantQuestion', { participantName }) }
</div>
<div className = { classes.container }>
<div className = { classes.row }>
{/* @ts-ignore */}
{emoji.slice(0, 4).map((e: Array<string>) =>
(<div
className = { classes.item }
key = { e.toString() }>
<div className = { classes.emoji }>{ e[0] }</div>
<div>{ e[1].charAt(0).toUpperCase() + e[1].slice(1) }</div>
</div>))}
</div>
<div className = { classes.row }>
{/* @ts-ignore */}
{emoji.slice(4, 7).map((e: Array<string>) =>
(<div
className = { classes.item }
key = { e.toString() }>
<div className = { classes.emoji }>{ e[0] } </div>
<div>{ e[1].charAt(0).toUpperCase() + e[1].slice(1) }</div>
</div>))}
</div>
</div>
</Dialog>
);
}
/**
* Notifies this ParticipantVerificationDialog that it has been dismissed by cancel.
*
* @private
* @returns {void}
*/
_onDismissed() {
this.props.dispatch(participantVerified(this.props.pId, false));
return true;
}
/**
* Notifies this ParticipantVerificationDialog that it has been dismissed with confirmation.
*
* @private
* @returns {void}
*/
_onConfirmed() {
this.props.dispatch(participantVerified(this.props.pId, true));
return true;
}
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {IReduxState} state - The Redux state.
* @param {IProps} ownProps - The own props of the component.
* @returns {IProps}
*/
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const participant = getParticipantById(state, ownProps.pId);
return {
sas: ownProps.sas,
pId: ownProps.pId,
participantName: participant?.name
};
}
export default translate(connect(_mapStateToProps)(
// @ts-ignore
withStyles(styles)(ParticipantVerificationDialog)));

View File

@@ -1,7 +1,9 @@
import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { getParticipantCount } from '../base/participants/functions';
import { getParticipantById, getParticipantCount } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { MAX_MODE_LIMIT, MAX_MODE_THRESHOLD } from './constants';
/**
@@ -55,3 +57,19 @@ export function isMaxModeThresholdReached(stateful: IStateful) {
return participantCount >= MAX_MODE_LIMIT + MAX_MODE_THRESHOLD;
}
/**
* Returns whether e2ee is enabled by the backend.
*
* @param {Object} state - The redux state.
* @param {string} pId - The participant id.
* @returns {boolean}
*/
export function displayVerification(state: IReduxState, pId: string) {
const { conference } = state['features/base/conference'];
const participant = getParticipantById(state, pId);
return Boolean(conference?.isE2EEEnabled()
&& participant?.e2eeVerificationAvailable
&& participant?.e2eeVerified === undefined);
}

View File

@@ -4,6 +4,8 @@ import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { openDialog } from '../base/dialog/actions';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import { participantUpdated } from '../base/participants/actions';
import {
@@ -17,13 +19,15 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
import { SET_MEDIA_ENCRYPTION_KEY, TOGGLE_E2EE } from './actionTypes';
import { PARTICIPANT_VERIFIED, SET_MEDIA_ENCRYPTION_KEY, START_VERIFICATION, TOGGLE_E2EE } from './actionTypes';
import { setE2EEMaxMode, setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
import ParticipantVerificationDialog from './components/ParticipantVerificationDialog';
import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants';
import { isMaxModeReached, isMaxModeThresholdReached } from './functions';
import logger from './logger';
import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds';
/**
* Middleware that captures actions related to E2EE.
*
@@ -239,6 +243,18 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
break;
}
case PARTICIPANT_VERIFIED: {
const { isVerified, pId } = action;
conference?.markParticipantVerified(pId, isVerified);
break;
}
case START_VERIFICATION: {
conference?.startVerification(action.pId);
break;
}
}
return next(action);
@@ -254,6 +270,29 @@ StateListenerRegistry.register(
if (previousConference) {
dispatch(toggleE2EE(false));
}
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE, (pId: string) => {
dispatch(participantUpdated({
e2eeVerificationAvailable: true,
id: pId
}));
});
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_READY, (pId: string, sas: object) => {
dispatch(openDialog(ParticipantVerificationDialog, { pId,
sas }));
});
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED,
(pId: string, success: boolean, message: string) => {
if (message) {
logger.warn('E2EE_VERIFICATION_COMPLETED warning', message);
}
dispatch(participantUpdated({
e2eeVerified: success,
id: pId
}));
});
});
/**

View File

@@ -20,6 +20,10 @@ export interface IE2EEState {
maxMode: string;
}
export interface ISas {
emoji: Array<string>;
}
/**
* Reduces the Redux actions of the feature features/e2ee.
*/

View File

@@ -17,12 +17,10 @@ import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
import { IParticipant } from '../../../base/participants/types';
import { connect } from '../../../base/redux/functions';
import { shouldHideSelfView } from '../../../base/settings/functions.web';
// @ts-ignore
import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
// @ts-ignore
import { getCurrentLayout } from '../../../video-layout';
import { LAYOUTS } from '../../../video-layout/constants';
import { getCurrentLayout } from '../../../video-layout/functions.web';
import {
setFilmstripVisible,
setTopPanelVisible,
@@ -30,7 +28,6 @@ import {
setUserFilmstripWidth,
setUserIsResizing,
setVisibleRemoteParticipants
// @ts-ignore
} from '../../actions';
import {
ASPECT_RATIO_BREAKPOINT,
@@ -46,7 +43,6 @@ import {
getVerticalViewMaxWidth,
isStageFilmstripTopPanel,
shouldRemoteVideosBeVisible
// @ts-ignore
} from '../../functions';
// @ts-ignore

View File

@@ -6,8 +6,6 @@ import { IReduxState } from '../../../app/types';
import { IconPin } from '../../../base/icons/svg';
import { getParticipantById } from '../../../base/participants/functions';
import BaseIndicator from '../../../base/react/components/web/BaseIndicator';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { getPinnedActiveParticipants, isStageFilmstripAvailable } from '../../functions.web';
/**

View File

@@ -39,10 +39,8 @@ import { hideGif, showGif } from '../../../gifs/actions';
import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
// @ts-ignore
import { PresenceLabel } from '../../../presence-status';
// @ts-ignore
import { getCurrentLayout } from '../../../video-layout';
import { LAYOUTS } from '../../../video-layout/constants';
// @ts-ignore
import { getCurrentLayout } from '../../../video-layout/functions.web';
import { togglePinStageParticipant } from '../../actions';
import {
DISPLAY_MODE_TO_CLASS_NAME,
@@ -60,7 +58,6 @@ import {
isStageFilmstripAvailable,
isVideoPlayable,
showGridInVerticalView
// @ts-ignore
} from '../../functions';
// @ts-ignore
@@ -1188,7 +1185,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any): Object {
const _audioTrack = isLocal
? getLocalAudioTrack(tracks)
: getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
const _currentLayout = getCurrentLayout(state);
const _currentLayout = getCurrentLayout(state) ?? '';
let size: any = {};
let _isMobilePortrait = false;
const {

View File

@@ -1,5 +1,3 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';

View File

@@ -1,5 +1,3 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@@ -9,11 +7,11 @@ import { isMobileBrowser } from '../../../base/environment/utils';
import { isScreenShareParticipantById } from '../../../base/participants/functions';
import ConnectionIndicator from '../../../connection-indicator/components/web/ConnectionIndicator';
import { STATS_POPOVER_POSITION, THUMBNAIL_TYPE } from '../../constants';
// @ts-ignore
import { getIndicatorsTooltipPosition } from '../../functions.web';
import PinnedIndicator from './PinnedIndicator';
import RaisedHandIndicator from './RaisedHandIndicator';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import StatusIndicators from './StatusIndicators';
import VideoMenuTriggerButton from './VideoMenuTriggerButton';

View File

@@ -4,8 +4,6 @@ import { getParticipantCountWithFake } from '../base/participants/functions';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { clientResized } from '../base/responsive-ui/actions';
import { shouldHideSelfView } from '../base/settings/functions.web';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { selectParticipantInLargeVideo } from '../large-video/actions.any';
import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { setOverflowDrawer } from '../toolbox/actions.web';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import { GiphyFetch, TrendingOptions } from '@giphy/js-fetch-api';
import { Grid } from '@giphy/react-components';
import React, { useCallback, useEffect, useState } from 'react';
@@ -13,8 +12,8 @@ import Input from '../../../base/ui/components/web/Input';
import { sendMessage } from '../../../chat/actions.any';
import { SCROLL_SIZE } from '../../../filmstrip/constants';
import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
// @ts-ignore
import { setOverflowMenuVisible } from '../../../toolbox/actions.web';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
import { showOverflowDrawer } from '../../../toolbox/functions.web';

View File

@@ -12,8 +12,7 @@ import { connect } from '../../../../base/redux/functions';
import Dialog from '../../../../base/ui/components/web/Dialog';
import { isDynamicBrandingDataLoaded } from '../../../../dynamic-branding/functions.any';
import { isVpaasMeeting } from '../../../../jaas/functions';
// @ts-ignore
import { getActiveSession } from '../../../../recording';
import { getActiveSession } from '../../../../recording/functions';
// @ts-ignore
import { updateDialInNumbers } from '../../../actions';
import {

View File

@@ -4,8 +4,6 @@ import { useStore } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { translate } from '../../base/i18n/functions';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { setSeeWhatIsBeingShared } from '../actions.web';
const useStyles = makeStyles()(theme => {

View File

@@ -7,8 +7,6 @@ import { isLocalParticipantModerator } from '../../../base/participants/function
import { connect } from '../../../base/redux/functions';
import Switch from '../../../base/ui/components/web/Switch';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { toggleLobbyMode } from '../../actions';
interface IProps extends WithTranslation {

View File

@@ -1,9 +1,9 @@
/* eslint-disable lines-around-comment */
import ReducerRegistry from '../../base/redux/ReducerRegistry';
import { set } from '../../base/redux/functions';
// @ts-ignore
import CallKit from './CallKit';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import ConnectionService from './ConnectionService';
import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';

View File

@@ -1,5 +1,7 @@
import { IStore } from '../app/types';
import { executeTrackOperation } from '../base/tracks/actions';
import { getLocalJitsiAudioTrack } from '../base/tracks/functions';
import { TrackOperationType } from '../base/tracks/types';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { NoiseSuppressionEffect } from '../stream-effects/noise-suppression/NoiseSuppressionEffect';
@@ -48,30 +50,32 @@ export function toggleNoiseSuppression(): any {
*/
export function setNoiseSuppressionEnabled(enabled: boolean): any {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const localAudio = getLocalJitsiAudioTrack(state);
const noiseSuppressionEnabled = isNoiseSuppressionEnabled(state);
logger.info(`Attempting to set noise suppression enabled state: ${enabled}`);
try {
if (enabled && !noiseSuppressionEnabled) {
if (!canEnableNoiseSuppression(state, dispatch, localAudio)) {
return;
await dispatch(executeTrackOperation(TrackOperationType.Audio, async () => {
const state = getState();
const localAudio = getLocalJitsiAudioTrack(state);
const noiseSuppressionEnabled = isNoiseSuppressionEnabled(state);
if (enabled && !noiseSuppressionEnabled) {
if (!canEnableNoiseSuppression(state, dispatch, localAudio)) {
return;
}
await localAudio.setEffect(new NoiseSuppressionEffect());
dispatch(setNoiseSuppressionEnabledState(true));
logger.info('Noise suppression enabled.');
} else if (!enabled && noiseSuppressionEnabled) {
await localAudio.setEffect(undefined);
dispatch(setNoiseSuppressionEnabledState(false));
logger.info('Noise suppression disabled.');
} else {
logger.warn(`Noise suppression enabled state already: ${enabled}`);
}
await localAudio.setEffect(new NoiseSuppressionEffect());
dispatch(setNoiseSuppressionEnabledState(true));
logger.info('Noise suppression enabled.');
} else if (!enabled && noiseSuppressionEnabled) {
await localAudio.setEffect(undefined);
dispatch(setNoiseSuppressionEnabledState(false));
logger.info('Noise suppression disabled.');
} else {
logger.warn(`Noise suppression enabled state already: ${enabled}`);
}
}));
} catch (error) {
logger.error(
`Failed to set noise suppression enabled to: ${enabled}`,

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import { IReduxState } from '../../app/types';
import { translate } from '../../base/i18n/functions';
import {
@@ -9,9 +8,9 @@ import { connect } from '../../base/redux/functions';
import {
AbstractButton,
type AbstractButtonProps
// @ts-ignore
} from '../../base/toolbox/components';
// @ts-ignore
import { setOverflowMenuVisible } from '../../toolbox/actions';
import { toggleNoiseSuppression } from '../actions';
import { isNoiseSuppressionEnabled } from '../functions';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
@@ -11,6 +10,7 @@ import ContextMenu from '../../../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../../../base/ui/components/web/ContextMenuItemGroup';
import { getBreakoutRooms } from '../../../../../breakout-rooms/functions';
import { showOverflowDrawer } from '../../../../../toolbox/functions.web';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import SendToRoomButton from '../../../../../video-menu/components/web/SendToRoomButton';
import { AVATAR_SIZE } from '../../../../constants';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -29,14 +28,10 @@ import {
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import {
openSettingsDialog,
shouldShowModeratorSettings
// @ts-ignore
} from '../../../settings';
import { openSettingsDialog } from '../../../settings/actions';
import { SETTINGS_TABS } from '../../../settings/constants';
// @ts-ignore
import { MuteEveryonesVideoDialog } from '../../../video-menu/components';
import { shouldShowModeratorSettings } from '../../../settings/functions.web';
import MuteEveryonesVideoDialog from '../../../video-menu/components/web/MuteEveryonesVideoDialog';
const useStyles = makeStyles()(theme => {
return {

View File

@@ -1,5 +1,3 @@
// eslint-disable-next-line lines-around-comment
import React, { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
@@ -12,8 +10,6 @@ import Button from '../../../base/ui/components/web/Button';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { showLobbyChatButton } from '../../../lobby/functions';
import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
import { useLobbyActions } from '../../hooks';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -11,6 +10,7 @@ import { IconCheck, IconCloseLarge } from '../../../base/icons/svg';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { admitMultiple } from '../../../lobby/actions.web';
import { getKnockingParticipants, getLobbyEnabled } from '../../../lobby/functions';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
import { showOverflowDrawer } from '../../../toolbox/functions.web';

View File

@@ -1,5 +1,4 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -18,8 +17,7 @@ import useContextMenu from '../../../base/ui/hooks/useContextMenu.web';
import { normalizeAccents } from '../../../base/util/strings.web';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { showOverflowDrawer } from '../../../toolbox/functions.web';
// @ts-ignore
import { muteRemote } from '../../../video-menu/actions.any';
import { muteRemote } from '../../../video-menu/actions.web';
import { getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
import { useParticipantDrawer } from '../../hooks';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -14,8 +13,7 @@ import ClickableIcon from '../../../base/ui/components/web/ClickableIcon';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { findAncestorByClass } from '../../../base/ui/functions.web';
import { isAddBreakoutRoomButtonVisible } from '../../../breakout-rooms/functions';
// @ts-ignore
import { MuteEveryoneDialog } from '../../../video-menu/components/';
import MuteEveryoneDialog from '../../../video-menu/components/web/MuteEveryoneDialog';
import { close } from '../../actions.web';
import {
getParticipantsPaneOpen,
@@ -23,6 +21,7 @@ import {
isMuteAllVisible
} from '../../functions';
import { AddBreakoutRoomButton } from '../breakout-rooms/components/web/AddBreakoutRoomButton';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { RoomList } from '../breakout-rooms/components/web/RoomList';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { Text, View } from 'react-native';
import { useSelector } from 'react-redux';

View File

@@ -2,8 +2,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { makeStyles } from 'tss-react/mui';
import Icon from '../../../base/icons/components/Icon';
import { IconBurger } from '../../../base/icons/svg';
// @ts-ignore
import { Tooltip } from '../../../base/tooltip';
import Button from '../../../base/ui/components/web/Button';
@@ -26,7 +24,6 @@ const PollCreate = ({
addAnswer,
answers,
isSubmitDisabled,
moveAnswer,
onSubmit,
question,
removeAnswer,
@@ -142,37 +139,6 @@ const PollCreate = ({
}
}, [ answers, addAnswer, removeAnswer, requestFocus ]);
const [ grabbing, setGrabbing ] = useState(null);
const interchangeHeights = (i: number, j: number) => {
const h = answerInputs.current[i].scrollHeight;
answerInputs.current[i].style.height = `${answerInputs.current[j].scrollHeight}px`;
answerInputs.current[j].style.height = `${h}px`;
};
const onGrab = useCallback((i, ev) => {
if (ev.button !== 0) {
return;
}
setGrabbing(i);
window.addEventListener('mouseup', () => {
setGrabbing(_grabbing => {
requestFocus(_grabbing);
return null;
});
}, { once: true });
}, []);
const onMouseOver = useCallback(i => {
if (grabbing !== null && grabbing !== i) {
interchangeHeights(i, grabbing);
moveAnswer(grabbing, i);
setGrabbing(i);
}
}, []);
const autogrow = (ev: React.ChangeEvent<HTMLTextAreaElement>) => {
const el = ev.target;
@@ -207,9 +173,8 @@ const PollCreate = ({
<ol className = 'poll-answer-field-list'>
{answers.map((answer: any, i: number) =>
(<li
className = { `poll-answer-field${grabbing === i ? ' poll-dragged' : ''}` }
key = { i }
onMouseOver = { () => onMouseOver(i) }>
className = 'poll-answer-field'
key = { i }>
<span className = 'poll-create-label'>
{ t('polls.create.pollOption', { index: i + 1 })}
</span>
@@ -225,13 +190,6 @@ const PollCreate = ({
required = { true }
rows = { 1 }
value = { answer } />
<button
className = 'poll-drag-handle'
onMouseDown = { ev => onGrab(i, ev) }
tabIndex = { -1 }
type = 'button'>
<Icon src = { IconBurger } />
</button>
</div>
{ answers.length > 2

View File

@@ -1,5 +1,4 @@
/* eslint-disable lines-around-comment */
// @ts-ignore
import React from 'react';
import Button from '../../../base/ui/components/web/Button';

View File

@@ -1,5 +1,4 @@
// @flow
// @ts-expect-error
import { Transport } from '../../../modules/transport';
import {
@@ -30,7 +29,7 @@ export function suspendDetected() {
* type: string
* }}
*/
export function setTransport(transport: ?Transport) {
export function setTransport(transport?: Transport) {
return {
type: SET_TRANSPORT,
transport

View File

@@ -1,16 +1,11 @@
// @flow
import {
PostMessageTransportBackend,
Transport
} from '../../../modules/transport';
// @ts-expect-error
import { PostMessageTransportBackend, Transport } from '../../../modules/transport';
import {
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../base/conference';
import { MiddlewareRegistry } from '../base/redux';
import { destroyLocalTracks } from '../base/tracks';
} from '../base/conference/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { destroyLocalTracks } from '../base/tracks/actions';
import { SUSPEND_DETECTED } from './actionTypes';
import {
@@ -18,8 +13,6 @@ import {
suspendDetected
} from './actions';
declare var APP: Object;
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
const { dispatch, getState } = store;
@@ -34,7 +27,7 @@ MiddlewareRegistry.register(store => next => action => {
})
});
transport.on('event', event => {
transport.on('event', (event: { event: string; name: string; }) => {
if (event && event.name === 'power-monitor' && event.event === 'suspend') {
dispatch(suspendDetected());

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import { v4 as uuidv4 } from 'uuid';
import { IStore } from '../app/types';
@@ -10,13 +9,16 @@ import { MEDIA_TYPE } from '../base/media/constants';
import { isVideoMutedByUser } from '../base/media/functions';
import { updateSettings } from '../base/settings/actions';
import { replaceLocalTrack, trackAdded } from '../base/tracks/actions';
import { executeTrackOperation } from '../base/tracks/actions.web';
import {
createLocalTracksF,
getLocalAudioTrack,
getLocalTracks,
getLocalVideoTrack
} from '../base/tracks/functions';
import { TrackOperationType } from '../base/tracks/types';
import { openURLInBrowser } from '../base/util/openURLInBrowser';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { executeDialOutRequest, executeDialOutStatusRequest, getDialInfoPageURL } from '../invite/functions';
import { showErrorNotification } from '../notifications/actions';
@@ -228,31 +230,35 @@ export function joinConference(options?: Object, ignoreJoiningInProgress = false
dispatch(setJoiningInProgress(true));
}
const state = getState();
let localTracks = getLocalTracks(state['features/base/tracks']);
options && dispatch(updateConfig(options));
// Do not signal audio/video tracks if the user joins muted.
for (const track of localTracks) {
// Always add the audio track on Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted.
if (track.muted
&& !(browser.isWebKitBased() && track.jitsiTrack && track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)) {
try {
await dispatch(replaceLocalTrack(track.jitsiTrack, null));
} catch (error) {
logger.error(`Failed to replace local track (${track.jitsiTrack}) with null: ${error}`);
const jitsiTracks = await dispatch(executeTrackOperation(TrackOperationType.AudioVideo, async () => {
const state = getState();
let localTracks = getLocalTracks(state['features/base/tracks']);
// Do not signal audio/video tracks if the user joins muted.
for (const track of localTracks) {
// Always add the audio track on Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted.
if (track.muted
&& !(browser.isWebKitBased()
&& track.jitsiTrack
&& track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)) {
try {
await dispatch(replaceLocalTrack(track.jitsiTrack, null));
} catch (error) {
logger.error(`Failed to replace local track (${track.jitsiTrack}) with null: ${error}`);
}
}
}
}
// Re-fetch the local tracks after muted tracks have been removed above.
// This is needed, because the tracks are effectively disposed by the replaceLocalTrack and should not be used
// anymore.
localTracks = getLocalTracks(getState()['features/base/tracks']);
// Re-fetch the local tracks after muted tracks have been removed above.
// This is needed, because the tracks are effectively disposed by the
// replaceLocalTrack and should not be used anymore.
localTracks = getLocalTracks(getState()['features/base/tracks']);
const jitsiTracks = localTracks.map((t: any) => t.jitsiTrack);
return localTracks.map((t: any) => t.jitsiTrack);
}));
APP.conference.prejoinStart(jitsiTracks);
};
@@ -288,16 +294,19 @@ export function joinConferenceWithoutAudio() {
}
dispatch(setJoiningInProgress(true));
const tracks = state['features/base/tracks'];
const audioTrack = getLocalAudioTrack(tracks)?.jitsiTrack;
if (audioTrack) {
try {
await dispatch(replaceLocalTrack(audioTrack, null));
} catch (error) {
logger.error(`Failed to replace local audio with null: ${error}`);
await dispatch(executeTrackOperation(TrackOperationType.Audio, async () => {
const tracks = getState()['features/base/tracks'];
const audioTrack = getLocalAudioTrack(tracks)?.jitsiTrack;
if (audioTrack) {
try {
await dispatch(replaceLocalTrack(audioTrack, null));
} catch (error) {
logger.error(`Failed to replace local audio with null: ${error}`);
}
}
}
}));
dispatch(joinConference({
startSilent: true
@@ -357,17 +366,19 @@ function prejoinInitialized() {
export function replaceAudioTrackById(deviceId: string) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
try {
const tracks = getState()['features/base/tracks'];
const newTrack = await createLocalTrack('audio', deviceId);
const oldTrack = getLocalAudioTrack(tracks)?.jitsiTrack;
const micDeviceId = newTrack.getDeviceId();
await dispatch(executeTrackOperation(TrackOperationType.Audio, async () => {
const tracks = getState()['features/base/tracks'];
const newTrack = await createLocalTrack('audio', deviceId);
const oldTrack = getLocalAudioTrack(tracks)?.jitsiTrack;
const micDeviceId = newTrack.getDeviceId();
logger.info(`Switching audio input device to ${micDeviceId}`);
dispatch(replaceLocalTrack(oldTrack, newTrack)).then(() => {
dispatch(updateSettings({
micDeviceId
}));
});
logger.info(`Switching audio input device to ${micDeviceId}`);
await dispatch(replaceLocalTrack(oldTrack, newTrack)).then(() => {
dispatch(updateSettings({
micDeviceId
}));
});
}));
} catch (err) {
dispatch(setDeviceStatusWarning('prejoin.audioTrackError'));
logger.log('Error replacing audio track', err);
@@ -384,24 +395,28 @@ export function replaceAudioTrackById(deviceId: string) {
export function replaceVideoTrackById(deviceId: string) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
try {
const tracks = getState()['features/base/tracks'];
const wasVideoMuted = isVideoMutedByUser(getState());
const [ newTrack ] = await createLocalTracksF(
{ cameraDeviceId: deviceId,
devices: [ 'video' ] },
{ dispatch,
getState }
);
const oldTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
const cameraDeviceId = newTrack.getDeviceId();
await dispatch(executeTrackOperation(TrackOperationType.Video, async () => {
const tracks = getState()['features/base/tracks'];
const wasVideoMuted = isVideoMutedByUser(getState());
const [ newTrack ] = await createLocalTracksF(
{ cameraDeviceId: deviceId,
devices: [ 'video' ] },
{ dispatch,
getState }
);
const oldTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
const cameraDeviceId = newTrack.getDeviceId();
logger.info(`Switching camera to ${cameraDeviceId}`);
dispatch(replaceLocalTrack(oldTrack, newTrack)).then(() => {
dispatch(updateSettings({
cameraDeviceId
}));
});
wasVideoMuted && newTrack.mute();
logger.info(`Switching camera to ${cameraDeviceId}`);
await dispatch(replaceLocalTrack(oldTrack, newTrack)).then(() => {
dispatch(updateSettings({
cameraDeviceId
}));
});
if (wasVideoMuted) {
await newTrack.mute();
}
}));
} catch (err) {
dispatch(setDeviceStatusWarning('prejoin.videoTrackError'));
logger.log('Error replacing video track', err);

View File

@@ -1,5 +1,3 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
@@ -9,6 +7,7 @@ import { Avatar } from '../../../../base/avatar';
import { translate } from '../../../../base/i18n/functions';
import Icon from '../../../../base/icons/components/Icon';
import { IconCloseLarge } from '../../../../base/icons/svg';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import Label from '../Label';

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import { Theme } from '@mui/material';
import { ClassNameMap, withStyles } from '@mui/styles';
import clsx from 'clsx';
@@ -14,10 +13,10 @@ import { translate } from '../../../base/i18n/functions';
import { raiseHand } from '../../../base/participants/actions';
import { getLocalParticipant, hasRaisedHand } from '../../../base/participants/functions';
import GifsMenu from '../../../gifs/components/web/GifsMenu';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import GifsMenuButton from '../../../gifs/components/web/GifsMenuButton';
import { isGifEnabled, isGifsMenuOpen } from '../../../gifs/functions';
// @ts-ignore
import { dockToolbox } from '../../../toolbox/actions.web';
import { addReactionToBuffer } from '../../actions.any';
import { toggleReactionsMenuVisibility } from '../../actions.web';

Some files were not shown because too many files have changed in this diff Show More