Compare commits

...

26 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
a62cddff73 fix(rnsdk) update README 2023-07-18 11:18:45 +02:00
Saúl Ibarra Corretgé
555be6c229 chore(deps) simplify overrides 2023-07-18 11:10:58 +02:00
Saúl Ibarra Corretgé
3f5e6883b5 chore(deps) redux@7.2.9
Fixes a warning due to the React version dependency
2023-07-18 11:10:58 +02:00
Saúl Ibarra Corretgé
ddcf90e95c feat(rnsdk) add npm ignore file 2023-07-18 11:10:58 +02:00
Saúl Ibarra Corretgé
683e9e72b9 fix(rnsdk,deps) update dependencies 2023-07-18 11:10:58 +02:00
Saúl Ibarra Corretgé
123eaf77fa fix(rnsdk) drop unnecessary Swift files and dependencies 2023-07-18 11:10:58 +02:00
damencho
bb29f20a07 fix: Fixes joining visitor.
The dispatches were not ordered and sometimes disconnect was executed just after connect and you end up in disconnected state.
2023-07-17 14:52:01 -05:00
Saúl Ibarra Corretgé
866390ece1 fix(android) fix crash when pending intent is created without flags
Fix for this crash:

~~~
Fatal Exception: java.lang.IllegalArgumentException: org.jitsi.meet: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality depends on the PendingIntent being mutable, e.g. if it needs to be used with inline replies or bubbles.
       at android.app.PendingIntent.checkFlags(PendingIntent.java:377)
       at android.app.PendingIntent.getActivityAsUser(PendingIntent.java:460)
       at android.app.PendingIntent.getActivity(PendingIntent.java:446)
       at android.app.PendingIntent.getActivity(PendingIntent.java:410)
       at com.google.android.gms.common.GoogleApiAvailabilityLight.getErrorResolutionPendingIntent(GoogleApiAvailabilityLight.java:25)
       at com.google.android.gms.common.GoogleApiAvailabilityLight.getErrorResolutionPendingIntent(GoogleApiAvailabilityLight.java:21)
       at com.google.android.gms.common.GoogleApiAvailability.getErrorResolutionPendingIntent(GoogleApiAvailability.java:97)
       at com.google.android.gms.common.GoogleApiAvailability.getErrorResolutionPendingIntent(GoogleApiAvailability.java:100)
       at com.google.android.gms.common.GoogleApiAvailability.zaa(GoogleApiAvailability.java:41)
       at com.google.android.gms.common.api.internal.GoogleApiManager.zac(GoogleApiManager.java:214)
       at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.onConnectionFailed(GoogleApiManager.java:86)
       at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.connect(GoogleApiManager.java:219)
       at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.zaa(GoogleApiManager.java:112)
       at com.google.android.gms.common.api.internal.GoogleApiManager.handleMessage(GoogleApiManager.java:145)
       at android.os.Handler.dispatchMessage(Handler.java:107)
       at com.google.android.gms.internal.base.zap.dispatchMessage(zap.java:8)
       at android.os.Looper.loopOnce(Looper.java:238)
       at android.os.Looper.loop(Looper.java:357)
       at android.os.HandlerThread.run(HandlerThread.java:67)
~~~
2023-07-17 18:16:46 +02:00
Robert Pintilii
5b844e45e3 ref(chat) Move styles from SCSS to JSS (#13559) 2023-07-17 16:01:24 +03:00
Saúl Ibarra Corretgé
1c80771405 chore(rn,deps) react-native@0.69.11 2023-07-17 15:00:01 +02:00
Saúl Ibarra Corretgé
d42cbbd9f8 fix(android) fix React-Native POM file when publishing
For some reason the packaging mode changed from AAR to POM after 0.68,
and dependencies are now marked optional, when they are not.

Fixes: https://github.com/jitsi/jitsi-meet/issues/13566
2023-07-17 15:00:01 +02:00
Robert Pintilii
18873a9659 ref(feedback) Convert dialog to function component (#13564) 2023-07-17 11:21:19 +03:00
Robert Pintilii
7859397790 ref(chat) Refactor ChatMessage component (#13556)
Remove Abstract class
Convert web component to function component
2023-07-17 10:47:54 +03:00
Дамян Минков
bc23f9cd33 feat: Drops connection on prejoin screen. (#13538)
* feat: Drops connection on prejoin screen.

Refactors connection logic to reuse already existing logic from mobile. Connection is now established just before joining the room.
Fixes some authentication logic with Login and Logout button in Profile tab.

* squash: Drops createInitialLocalTracksAndConnect as it no longer connects.

* squash: Shows an error on mobile and redirects to default.

* squash: Fixes review comments.

* squash: Fixes joining with prejoin disabled.

* squash: Fixes adding initial local tracks.

* squash: Fixes comments.

* squash: Drop no longer used method.

* squash: Fixes old web code imported into mobile builds.

* squash: Drop unused prop.

* squash: Drops recoverable flag on REDIRECT.

* squash: Drops unused variable and fix connection access.

* squash: Xmpp connect returns promise again.

* squash: Execute xmpp connect and creating local tracks in parallel.

* squash: Moves notification about problem jwt.

* squash: Moves startConference to conference.js for the no prejoin case.

And move the startConference in prejoin feature for the prejoin case.

* squash: Fix passing filtered tracks when starting conference with no prejoin.

* squash: Fix clearing listeners on connection established.

Keeps mobile behaviour after merging web and mobile.

* squash: Drops unused code.
2023-07-15 17:33:26 -05:00
Robert Pintilii
02f0057578 ref(styles) Move some styles from SCSS to JSS (#13565) 2023-07-14 15:58:07 +03:00
Calinteodor
63761d515a feat(base/flags): created flag to control unsafe room warning (#13560)
* feat(base/flags): created flag to control unsafe room warning
2023-07-14 11:42:15 +03:00
Jaya Allamsetty
5cc4b31f35 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1656.0.0+e0d3459a...v1659.0.0+5d322ea5
2023-07-13 22:59:26 -04:00
Robert Pintilii
a03bf2cb8e fix(local-rec) Download recording on meeting end (#13557) 2023-07-13 16:01:53 +03:00
Calin-Teodor
312902ea77 deps(patch/react-native-immersive): replaced removeListener deprecated method 2023-07-13 15:17:01 +03:00
Robert Pintilii
961a9236fd ref: remove some Abstract components (#13553) 2023-07-13 12:27:34 +03:00
damencho
364e63da14 feat: New prosody auth module using shared secret.
The same as username and password but ignoring the username. Useful for jigasi and jibri where the instances can use different usernames, but the same shared secret/password.
2023-07-12 12:39:00 -05:00
Calinteodor
27c62b3d78 sdk(react-native-sdk): error fixes (#13549)
* feat(mobile/background/react-native-sdk): replaced removeListener deprecated method and fixed some undefined errors
2023-07-12 17:28:30 +03:00
Robert Pintilii
51623b47f0 ref(CSS): Cleanup (#13554)
Remove unused styles
2023-07-12 15:51:56 +03:00
Robert Pintilii
824cfc0c9c ref(chat): Refactor Chat components (#13550)
Remove Abstract component
Convert web component to function component
2023-07-12 15:51:38 +03:00
Saúl Ibarra Corretgé
398e170e2d fix(settings) fix when devices tab is not visible
Fixes: https://github.com/jitsi/jitsi-meet/issues/13461
2023-07-11 21:47:57 +02:00
Saúl Ibarra Corretgé
f1de6887cd chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1654.0.0+782350e0...v1656.0.0+e0d3459a
2023-07-11 17:25:38 +02:00
115 changed files with 2398 additions and 3585 deletions

View File

@@ -81,7 +81,8 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
if (!rootProject.ext.libreBuild) {
implementation 'com.google.android.gms:play-services-auth:16.0.1'
// Sync with react-native-google-signin
implementation 'com.google.android.gms:play-services-auth:19.0.2'
// Firebase
// - Crashlytics

View File

@@ -29,6 +29,10 @@ if [[ $MVN_HTTP == 1 ]]; then
# Push React Native
echo "Pushing React Native ${RN_VERSION} to the Maven repo"
pushd ${THIS_DIR}/../../node_modules/react-native/android/com/facebook/react/react-native/${RN_VERSION}
cat react-native-${RN_VERSION}.pom \
| sed "s#<packaging>pom</packaging>#<packaging>aar</packaging>#" \
| sed "/<optional>/d" \
> react-native-${RN_VERSION}-fixed.pom
mvn \
deploy:deploy-file \
-Durl=${MVN_REPO} \
@@ -36,7 +40,7 @@ if [[ $MVN_HTTP == 1 ]]; then
-Dfile=react-native-${RN_VERSION}-release.aar \
-Dpackaging=aar \
-DgeneratePom=false \
-DpomFile=react-native-${RN_VERSION}.pom || true
-DpomFile=react-native-${RN_VERSION}-fixed.pom || true
popd
# Push JSC
echo "Pushing JSC ${JSC_VERSION} to the Maven repo"
@@ -55,13 +59,17 @@ else
if [[ ! -d ${MVN_REPO}/com/facebook/react/react-native/${RN_VERSION} ]]; then
echo "Pushing React Native ${RN_VERSION} to the Maven repo"
pushd ${THIS_DIR}/../../node_modules/react-native/android/com/facebook/react/react-native/${RN_VERSION}
cat react-native-${RN_VERSION}.pom \
| sed "s#<packaging>pom</packaging>#<packaging>aar</packaging>#" \
| sed "/<optional>/d" \
> react-native-${RN_VERSION}-fixed.pom
mvn \
deploy:deploy-file \
-Durl=${MVN_REPO} \
-Dfile=react-native-${RN_VERSION}-release.aar \
-Dpackaging=aar \
-DgeneratePom=false \
-DpomFile=react-native-${RN_VERSION}.pom
-DpomFile=react-native-${RN_VERSION}-fixed.pom
popd
fi

View File

@@ -4,12 +4,8 @@ import { jitsiLocalStorage } from '@jitsi/js-utils';
import Logger from '@jitsi/logger';
import EventEmitter from 'events';
import { openConnection } from './connection';
import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from './modules/UI/UIErrors';
import AuthHandler from './modules/UI/authentication/AuthHandler';
import UIUtil from './modules/UI/util/UIUtil';
import VideoLayout from './modules/UI/videolayout/VideoLayout';
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
import Recorder from './modules/recorder/Recorder';
import { createTaskQueue } from './modules/util/helpers';
@@ -37,7 +33,7 @@ import {
conferenceSubjectChanged,
conferenceTimestampChanged,
conferenceUniqueIdSet,
conferenceWillJoin,
conferenceWillInit,
conferenceWillLeave,
dataChannelClosed,
dataChannelOpened,
@@ -57,12 +53,10 @@ import {
commonUserJoinedHandling,
commonUserLeftHandling,
getConferenceOptions,
getVisitorOptions,
restoreConferenceOptions,
sendLocalParticipant
} from './react/features/base/conference/functions';
import { overwriteConfig } from './react/features/base/config/actions';
import { getReplaceParticipant } from './react/features/base/config/functions';
import { connect } from './react/features/base/connection/actions.web';
import {
checkAndNotifyForNewDevice,
getAvailableDevices,
@@ -77,15 +71,12 @@ import {
import {
JitsiConferenceErrors,
JitsiConferenceEvents,
JitsiConnectionErrors,
JitsiConnectionEvents,
JitsiE2ePingEvents,
JitsiMediaDevicesEvents,
JitsiTrackErrors,
JitsiTrackEvents,
browser
} from './react/features/base/lib-jitsi-meet';
import { isFatalJitsiConnectionError } from './react/features/base/lib-jitsi-meet/functions';
import {
gumPending,
setAudioAvailable,
@@ -145,7 +136,12 @@ import { maybeOpenFeedbackDialog, submitFeedback } from './react/features/feedba
import { initKeyboardShortcuts } from './react/features/keyboard-shortcuts/actions';
import { maybeSetLobbyChatMessageListener } from './react/features/lobby/actions.any';
import { setNoiseSuppressionEnabled } from './react/features/noise-suppression/actions';
import { hideNotification, showNotification, showWarningNotification } from './react/features/notifications/actions';
import {
hideNotification,
showErrorNotification,
showNotification,
showWarningNotification
} from './react/features/notifications/actions';
import {
DATA_CHANNEL_CLOSED_NOTIFICATION_ID,
NOTIFICATION_TIMEOUT_TYPE
@@ -153,7 +149,7 @@ import {
import { isModerationNotificationDisplayed } from './react/features/notifications/functions';
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay/actions';
import { suspendDetected } from './react/features/power-monitor/actions';
import { initPrejoin, makePrecallTest, setJoiningInProgress } from './react/features/prejoin/actions';
import { initPrejoin, makePrecallTest } from './react/features/prejoin/actions';
import { isPrejoinPageVisible } from './react/features/prejoin/functions';
import { disableReceiver, stopReceiver } from './react/features/remote-control/actions';
import { setScreenAudioShareState } from './react/features/screen-share/actions.web';
@@ -164,7 +160,6 @@ import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise'
import { endpointMessageReceived } from './react/features/subtitles/actions.any';
import { handleToggleVideoMuted } from './react/features/toolbox/actions.any';
import { muteLocal } from './react/features/video-menu/actions.any';
import { setIAmVisitor } from './react/features/visitors/actions';
import { iAmVisitor } from './react/features/visitors/functions';
import UIEvents from './service/UI/UIEvents';
@@ -173,25 +168,6 @@ const logger = Logger.getLogger(__filename);
const eventEmitter = new EventEmitter();
let room;
let connection;
/**
* The promise is used when the prejoin screen is shown.
* While the user configures the devices the connection can be made.
*
* @type {Promise<Object>}
* @private
*/
let _connectionPromise;
/**
* We are storing the resolve function of a Promise that waits for the _connectionPromise to be created. This is needed
* when the prejoin button was pressed before the conference object was initialized and the _connectionPromise has not
* been initialized when we tried to execute prejoinStart. In this case in prejoinStart we create a new Promise, assign
* the resolve function to this variable and wait for the promise to resolve before we continue. The
* _onConnectionPromiseCreated will be called once the _connectionPromise is created.
*/
let _onConnectionPromiseCreated;
/*
* Logic to open a desktop picker put on the window global for
@@ -213,26 +189,6 @@ const commands = {
ETHERPAD: 'etherpad'
};
/**
* Open Connection. When authentication failed it shows auth dialog.
* @param roomName the room name to use
* @returns Promise<JitsiConnection>
*/
function connect(roomName) {
return openConnection({
retry: true,
roomName
})
.catch(err => {
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED) {
APP.UI.notifyTokenAuthFailed();
} else {
APP.UI.notifyConnectionFailed(err);
}
throw err;
});
}
/**
* Share data to other users.
* @param command the command
@@ -326,63 +282,25 @@ class ConferenceConnector {
break;
}
// not enough rights to create conference
case JitsiConferenceErrors.AUTHENTICATION_REQUIRED: {
const replaceParticipant = getReplaceParticipant(APP.store.getState());
// Schedule reconnect to check if someone else created the room.
this.reconnectTimeout = setTimeout(() => {
APP.store.dispatch(conferenceWillJoin(room));
room.join(null, replaceParticipant);
}, 5000);
const { password }
= APP.store.getState()['features/base/conference'];
AuthHandler.requireAuth(room, password);
break;
}
case JitsiConferenceErrors.RESERVATION_ERROR: {
const [ code, msg ] = params;
APP.UI.notifyReservationError(code, msg);
break;
}
case JitsiConferenceErrors.REDIRECTED: {
const newConfig = getVisitorOptions(APP.store.getState(), params);
if (!newConfig) {
logger.warn('Not redirected missing params');
break;
}
const [ vnode ] = params;
APP.store.dispatch(overwriteConfig(newConfig))
.then(() => this._conference.leaveRoom())
.then(() => APP.store.dispatch(setIAmVisitor(Boolean(vnode))))
// we do not clear local tracks on error, so we need to manually clear them
.then(() => APP.store.dispatch(destroyLocalTracks()))
.then(() => {
// Reset VideoLayout. It's destroyed in features/video-layout/middleware.web.js so re-initialize it.
VideoLayout.initLargeVideo();
VideoLayout.resizeVideoArea();
connect(this._conference.roomName).then(con => {
this._conference.startConference(con, []);
});
});
APP.store.dispatch(showErrorNotification({
descriptionArguments: {
code,
msg
},
descriptionKey: 'dialog.reservationErrorMsg',
titleKey: 'dialog.reservationError'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
break;
}
case JitsiConferenceErrors.GRACEFUL_SHUTDOWN:
APP.UI.notifyGracefulShutdown();
APP.store.dispatch(showErrorNotification({
descriptionKey: 'dialog.gracefulShutdown',
titleKey: 'dialog.serviceUnavailable'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
break;
// FIXME FOCUS_DISCONNECTED is a confusing event name.
@@ -408,31 +326,9 @@ class ConferenceConnector {
// FIXME the conference should be stopped by the library and not by
// the app. Both the errors above are unrecoverable from the library
// perspective.
room.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).then(() => connection.disconnect());
room.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).then(() => APP.connection.disconnect());
break;
case JitsiConferenceErrors.CONFERENCE_MAX_USERS: {
APP.UI.notifyMaxUsersLimitReached();
// in case of max users(it can be from a visitor node), let's restore
// oldConfig if any as we will be back to the main prosody
const newConfig = restoreConferenceOptions(APP.store.getState());
if (newConfig) {
APP.store.dispatch(overwriteConfig(newConfig))
.then(() => this._conference.leaveRoom())
.then(() => {
_connectionPromise = connect(this._conference.roomName);
return _connectionPromise;
})
.then(con => {
APP.connection = connection = con;
});
}
break;
}
case JitsiConferenceErrors.INCOMPATIBLE_SERVER_VERSIONS:
APP.store.dispatch(reloadWithStoredParams());
break;
@@ -488,11 +384,11 @@ function disconnect() {
return Promise.resolve();
};
if (!connection) {
if (!APP.connection) {
return onDisconnected();
}
return connection.disconnect().then(onDisconnected, onDisconnected);
return APP.connection.disconnect().then(onDisconnected, onDisconnected);
}
/**
@@ -510,25 +406,6 @@ function setGUMPendingStateOnFailedTracks(tracks) {
APP.store.dispatch(gumPending(nonPendingTracks, IGUMPendingState.NONE));
}
/**
* Handles CONNECTION_FAILED events from lib-jitsi-meet.
*
* @param {JitsiConnectionError} error - The reported error.
* @returns {void}
* @private
*/
function _connectionFailedHandler(error) {
if (isFatalJitsiConnectionError(error)) {
APP.connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
_connectionFailedHandler);
if (room) {
APP.store.dispatch(conferenceWillLeave(room));
room.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR);
}
}
}
export default {
/**
* Flag used to delay modification of the muted status of local media tracks
@@ -547,7 +424,16 @@ export default {
/**
* Returns an object containing a promise which resolves with the created tracks &
* the errors resulting from that process.
*
* @param {object} options
* @param {boolean} options.startAudioOnly=false - if <tt>true</tt> then
* only audio track will be created and the audio only mode will be turned
* on.
* @param {boolean} options.startScreenSharing=false - if <tt>true</tt>
* should start with screensharing instead of camera video.
* @param {boolean} options.startWithAudioMuted - will start the conference
* without any audio tracks.
* @param {boolean} options.startWithVideoMuted - will start the conference
* without any video tracks.
* @returns {Promise<JitsiLocalTrack[]>, Object}
*/
createInitialLocalTracks(options = {}) {
@@ -725,37 +611,7 @@ export default {
}
},
/**
* Creates local media tracks and connects to a room. Will show error
* dialogs in case accessing the local microphone and/or camera failed. Will
* show guidance overlay for users on how to give access to camera and/or
* microphone.
* @param {string} roomName
* @param {object} options
* @param {boolean} options.startAudioOnly=false - if <tt>true</tt> then
* only audio track will be created and the audio only mode will be turned
* on.
* @param {boolean} options.startScreenSharing=false - if <tt>true</tt>
* should start with screensharing instead of camera video.
* @param {boolean} options.startWithAudioMuted - will start the conference
* without any audio tracks.
* @param {boolean} options.startWithVideoMuted - will start the conference
* without any video tracks.
* @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
*/
createInitialLocalTracksAndConnect(roomName, options = {}) {
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
return Promise.all([ tryCreateLocalTracks, connect(roomName) ])
.then(([ tracks, con ]) => {
this._displayErrorsForCreateInitialLocalTracks(errors);
return [ tracks, con ];
});
},
startConference(con, tracks) {
startConference(tracks) {
tracks.forEach(track => {
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|| (track.isVideoTrack() && this.isLocalVideoMuted())) {
@@ -768,9 +624,6 @@ export default {
}
});
con.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _connectionFailedHandler);
APP.connection = connection = con;
this._createRoom(tracks);
// if user didn't give access to mic or camera or doesn't have
@@ -860,17 +713,6 @@ export default {
};
if (isPrejoinPageVisible(state)) {
_connectionPromise = connect(roomName).then(c => {
// We want to initialize it early, in case of errors to be able to gather logs.
APP.connection = c;
return c;
});
if (_onConnectionPromiseCreated) {
_onConnectionPromiseCreated();
}
APP.store.dispatch(makePrecallTest(this._getConferenceOptions()));
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
@@ -897,41 +739,26 @@ export default {
return this._setLocalAudioVideoStreams(tracks);
}
const [ tracks, con ] = await this.createInitialLocalTracksAndConnect(roomName, initialOptions);
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
this._initDeviceList(true);
return Promise.all([
tryCreateLocalTracks.then(tr => {
this._displayErrorsForCreateInitialLocalTracks(errors);
const filteredTracks = handleInitialTracks(initialOptions, tracks);
return tr;
}).then(tr => {
this._initDeviceList(true);
setGUMPendingStateOnFailedTracks(filteredTracks);
const filteredTracks = handleInitialTracks(initialOptions, tr);
return this.startConference(con, filteredTracks);
},
setGUMPendingStateOnFailedTracks(filteredTracks);
/**
* Joins conference after the tracks have been configured in the prejoin screen.
*
* @param {Object[]} tracks - An array with the configured tracks
* @returns {void}
*/
async prejoinStart(tracks) {
if (!_connectionPromise) {
// The conference object isn't initialized yet. Wait for the promise to initialise.
await new Promise(resolve => {
_onConnectionPromiseCreated = resolve;
});
_onConnectionPromiseCreated = undefined;
}
let con;
try {
con = await _connectionPromise;
this.startConference(con, tracks);
} catch (error) {
logger.error(`An error occurred while trying to join a meeting from the prejoin screen: ${error}`);
APP.store.dispatch(setJoiningInProgress(false));
}
return filteredTracks;
}),
APP.store.dispatch(connect())
]).then(([ tracks, _ ]) => {
this.startConference(tracks).catch(logger.error);
});
},
/**
@@ -1413,9 +1240,7 @@ export default {
* Used by the Breakout Rooms feature to join a breakout room or go back to the main room.
*/
async joinRoom(roomName, options) {
// Reset VideoLayout. It's destroyed in features/video-layout/middleware.web.js so re-initialize it.
VideoLayout.initLargeVideo();
VideoLayout.resizeVideoArea();
APP.store.dispatch(conferenceWillInit());
// Restore initial state.
this._localTracksInitialized = false;
@@ -1440,7 +1265,7 @@ export default {
},
_createRoom(localTracks) {
room = connection.initJitsiConference(APP.conference.roomName, this._getConferenceOptions());
room = APP.connection.initJitsiConference(APP.conference.roomName, this._getConferenceOptions());
// Filter out the tracks that are muted (except on Safari).
const tracks = browser.isWebKitBased() ? localTracks : localTracks.filter(track => !track.isMuted());
@@ -1787,10 +1612,10 @@ export default {
titleKey = 'notify.screenShareNoAudioTitle';
}
APP.UI.messageHandler.showError({
APP.store.dispatch(showErrorNotification({
descriptionKey,
titleKey
});
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
},
/**
@@ -2184,21 +2009,6 @@ export default {
this.hangup(true);
});
// logout
APP.UI.addListener(UIEvents.LOGOUT, () => {
AuthHandler.logout(room).then(url => {
if (url) {
UIUtil.redirect(url);
} else {
this.hangup(true);
}
});
});
APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
AuthHandler.authenticate(room);
});
APP.UI.addListener(
UIEvents.VIDEO_DEVICE_CHANGED,
cameraDeviceId => {

View File

@@ -1,209 +0,0 @@
/* global APP, JitsiMeetJS, config */
import { jitsiLocalStorage } from '@jitsi/js-utils';
import Logger from '@jitsi/logger';
import { redirectToTokenAuthService } from './modules/UI/authentication/AuthHandler';
import { LoginDialog } from './react/features/authentication/components';
import { isTokenAuthEnabled } from './react/features/authentication/functions';
import {
connectionEstablished,
connectionFailed,
constructOptions
} from './react/features/base/connection/actions.web';
import { openDialog } from './react/features/base/dialog/actions';
import { setJWT } from './react/features/base/jwt/actions';
import {
JitsiConnectionErrors,
JitsiConnectionEvents
} from './react/features/base/lib-jitsi-meet';
import { isFatalJitsiConnectionError } from './react/features/base/lib-jitsi-meet/functions';
import { getCustomerDetails } from './react/features/jaas/actions.any';
import { getJaasJWT, isVpaasMeeting } from './react/features/jaas/functions';
import {
setPrejoinDisplayNameRequired
} from './react/features/prejoin/actions';
const logger = Logger.getLogger(__filename);
/**
* The feature announced so we can distinguish jibri participants.
*
* @type {string}
*/
export const DISCO_JIBRI_FEATURE = 'http://jitsi.org/protocol/jibri';
/**
* Try to open connection using provided credentials.
* @param {string} [id]
* @param {string} [password]
* @returns {Promise<JitsiConnection>} connection if
* everything is ok, else error.
*/
export async function connect(id, password) {
const state = APP.store.getState();
let { jwt } = state['features/base/jwt'];
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
if (!iAmRecorder && !iAmSipGateway && isVpaasMeeting(state)) {
await APP.store.dispatch(getCustomerDetails());
if (!jwt) {
jwt = await getJaasJWT(state);
APP.store.dispatch(setJWT(jwt));
}
}
const connection = new JitsiMeetJS.JitsiConnection(null, jwt, constructOptions(state));
if (config.iAmRecorder) {
connection.addFeature(DISCO_JIBRI_FEATURE);
}
return new Promise((resolve, reject) => {
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
handleConnectionEstablished);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
handleConnectionFailed);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
connectionFailedHandler);
connection.addEventListener(
JitsiConnectionEvents.DISPLAY_NAME_REQUIRED,
displayNameRequiredHandler
);
/* eslint-disable max-params */
/**
*
*/
function connectionFailedHandler(error, message, credentials, details) {
/* eslint-enable max-params */
APP.store.dispatch(
connectionFailed(
connection, {
credentials,
details,
message,
name: error
}));
if (isFatalJitsiConnectionError(error)) {
connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
connectionFailedHandler);
}
}
/**
*
*/
function unsubscribe() {
connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
handleConnectionEstablished);
connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
handleConnectionFailed);
}
/**
*
*/
function handleConnectionEstablished() {
APP.store.dispatch(connectionEstablished(connection, Date.now()));
unsubscribe();
resolve(connection);
}
/**
*
*/
function handleConnectionFailed(err) {
unsubscribe();
logger.error('CONNECTION FAILED:', err);
reject(err);
}
/**
* Marks the display name for the prejoin screen as required.
* This can happen if a user tries to join a room with lobby enabled.
*/
function displayNameRequiredHandler() {
APP.store.dispatch(setPrejoinDisplayNameRequired());
}
connection.connect({
id,
password
});
});
}
/**
* Open JitsiConnection using provided credentials.
* If retry option is true it will show auth dialog on PASSWORD_REQUIRED error.
*
* @param {object} options
* @param {string} [options.id]
* @param {string} [options.password]
* @param {string} [options.roomName]
* @param {boolean} [retry] if we should show auth dialog
* on PASSWORD_REQUIRED error.
*
* @returns {Promise<JitsiConnection>}
*/
export function openConnection({ id, password, retry, roomName }) {
const usernameOverride
= jitsiLocalStorage.getItem('xmpp_username_override');
const passwordOverride
= jitsiLocalStorage.getItem('xmpp_password_override');
if (usernameOverride && usernameOverride.length > 0) {
id = usernameOverride; // eslint-disable-line no-param-reassign
}
if (passwordOverride && passwordOverride.length > 0) {
password = passwordOverride; // eslint-disable-line no-param-reassign
}
return connect(id, password).catch(err => {
if (retry) {
const { jwt } = APP.store.getState()['features/base/jwt'];
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED && !jwt) {
return requestAuth(roomName);
}
}
throw err;
});
}
/**
* Show Authentication Dialog and try to connect with new credentials.
* If failed to connect because of PASSWORD_REQUIRED error
* then ask for password again.
* @param {string} [roomName] name of the conference room
*
* @returns {Promise<JitsiConnection>}
*/
function requestAuth(roomName) {
const config = APP.store.getState()['features/base/config'];
if (isTokenAuthEnabled(config)) {
// This Promise never resolves as user gets redirected to another URL
return new Promise(() => redirectToTokenAuthService(roomName));
}
return new Promise(resolve => {
const onSuccess = connection => {
resolve(connection);
};
APP.store.dispatch(
openDialog(LoginDialog, { onSuccess,
roomName })
);
});
}

View File

@@ -66,10 +66,6 @@ body, input, textarea, keygen, select, button {
font-family: $baseFontFamily !important;
}
#nowebrtc {
display:none;
}
button, input, select, textarea {
margin: 0;
vertical-align: baseline;
@@ -148,21 +144,6 @@ form {
z-index: 100;
}
.connected {
color: #21B9FC;
font-size: 12px;
}
.lastN, .disconnected {
color: #a3a3a3;
font-size: 12px;
}
#inviteLinkRef {
-webkit-user-select: text;
user-select: text;
}
/**
* Re-style default OS scrollbar.
*/
@@ -193,3 +174,16 @@ form {
.jitsi-icon svg path {
fill: inherit !important;
}
.sr-only {
border: 0 !important;
clip: rect(1px, 1px, 1px, 1px) !important;
clip-path: inset(50%) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important;
}

View File

@@ -1,35 +1,3 @@
#sideToolbarContainer {
background-color: $chatBackgroundColor;
flex-shrink: 0;
overflow: hidden;
position: relative;
transition: width .16s ease-in-out;
width: $sidebarWidth;
z-index: 300;
@media (max-width: 580px) {
height: 100vh;
height: -webkit-fill-available;
left: 0;
position: fixed;
right: 0;
top: 0;
width: auto;
}
}
.chat-panel {
display: flex;
flex-direction: column;
// extract header + tabs height
height: calc(100% - 119px);
}
.chat-panel-no-tabs {
// extract header height
height: calc(100% - 70px);
}
#chat-conversation-container {
// extract message input height
height: calc(100% - 64px);
@@ -76,27 +44,6 @@
}
}
.chat-header {
height: 70px;
position: relative;
width: 100%;
z-index: 1;
display: flex;
justify-content: space-between;
padding: 16px;
align-items: center;
box-sizing: border-box;
color: #fff;
font-weight: 600;
font-size: 24px;
line-height: 32px;
.jitsi-icon {
cursor: pointer;
}
}
.chat-input-container {
padding: 0 16px 24px;
}
@@ -112,61 +59,6 @@
margin-right: 8px;
}
.smiley-button {
display: flex;
align-items: center;
justify-content: center;
height: 38px;
width: 38px;
margin: 2px;
border-radius: 3px;
}
#chat-input .smiley-button {
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: #484A4F;
}
}
}
.remoteuser {
color: #B8C7E0;
}
.usrmsg-form {
flex: 1;
}
#usermsg {
-ms-overflow-style: none;
border: 0px none;
border-radius:0;
box-shadow: none;
color: white;
font-size: 14px;
padding: 10px;
overflow-y: auto;
resize: none;
scrollbar-width: none;
width: 100%;
word-break: break-word;
&::-webkit-scrollbar {
display: none;
}
}
#usermsg:hover {
border: 0px none;
box-shadow: none;
}
#usermsg:focus,
#usermsg:active {
border-bottom: 1px solid white;
padding-bottom: 8px;
}
#nickname {
text-align: center;
color: #9d9d9d;
@@ -174,11 +66,6 @@
margin: auto 0;
padding: 0 16px;
#nickname-title {
margin-bottom: 5px;
display: block;
}
label[for="nickinput"] {
> div > span {
color: #B8C7E0;
@@ -191,24 +78,6 @@
label {
line-height: 24px;
}
.enter-chat {
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
height: 40px;
background: #1B67EC;
border-radius: 3px;
color: #fff;
cursor: pointer;
&.disabled {
color: #AFB6BC;
background: #11336E;
pointer-events: none;
}
}
}
.mobile-browser {
@@ -216,14 +85,6 @@
input {
height: 48px;
}
.enter-chat {
height: 48px;
}
}
#usermsg {
font-size: 16px;
}
.chatmessage .usermessage {
@@ -231,32 +92,7 @@
}
}
.sideToolbarContainer {
* {
-webkit-user-select: text;
user-select: text;
}
}
.sr-only {
border: 0 !important;
clip: rect(1px, 1px, 1px, 1px) !important;
clip-path: inset(50%) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important;
}
.chatmessage {
&.localuser {
background-color: #484A4F;
border-radius: 6px 0px 6px 6px;
}
&.error {
border-radius: 0px;
@@ -288,13 +124,6 @@
padding: 2px;
}
#smileysarea {
display: flex;
max-height: 150px;
min-height: 35px;
overflow: hidden;
}
.smiley-input {
display: flex;
position: absolute;
@@ -345,10 +174,6 @@
cursor: pointer;
}
#usermsg::-webkit-scrollbar-track-piece {
background: #3a3a3a;
}
.chat-message-group {
&.local {
align-items: flex-end;

View File

@@ -28,18 +28,6 @@
margin-bottom: env(safe-area-inset-bottom, 0);
width: 100%;
.drawer-toggle {
display: flex;
justify-content: center;
align-items: center;
height: 44px;
cursor: pointer;
svg {
fill: none;
}
}
&#{&} .overflow-menu {
margin: auto;
font-size: 1.2em;
@@ -65,25 +53,10 @@
align-items: center;
}
&.unclickable {
cursor: default;
}
@media (hover: hover) and (pointer: fine) {
&.unclickable:hover {
background: inherit;
}
}
&.disabled {
cursor: initial;
color: #3b475c;
}
}
.profile-text {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}

View File

@@ -1,21 +0,0 @@
#e2ee-section {
display: flex;
flex-direction: column;
.description {
font-size: 13px;
margin: 15px 0;
}
.control-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
label {
font-size: 14px;
font-weight: bold;
}
}
}

View File

@@ -1,18 +1,4 @@
/*Initialize*/
div.loginmenu {
position: absolute;
margin: 0;
padding: 5px;
top: 40px;
left: 20px;
}
a.disabled {
color: gray !important;
pointer-events: none;
}
.loginmenu.extendedToolbarPopup {
top: 20px;
left: 40px;
}

View File

@@ -1,4 +1,3 @@
.filmstrip-toolbox,
.always-on-top-toolbox {
background-color: $newToolbarBackgroundColor;
border-radius: 3px;
@@ -29,7 +28,3 @@
transform: translateX(-50%);
padding: 3px !important;
}
.filmstrip-toolbox {
flex-direction: column;
}

View File

@@ -1,29 +0,0 @@
.jqistates {
font-size: 14px;
}
.jqistates h2 {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
font-size: 18px;
line-height: 25px;
text-align: center;
color: #424242;
}
.jqistates input {
margin: 10px 0;
}
.jqistates input[type='text'], input[type='password'] {
width: 100%;
}
button.jqidefaultbutton #inviteLinkRef {
color: #2c8ad2;
}
#inviteLinkRef {
-webkit-user-select: text;
user-select: text;
}

View File

@@ -75,6 +75,3 @@
margin-bottom: 36px;
width: 100%;
}
.navigate-section-list-empty {
text-align: center;
}

View File

@@ -1,15 +0,0 @@
.notice {
position: absolute;
left: 50%;
z-index: $zindex3;
margin-top: 6px;
@include transform(translateX(-50%));
&__message {
background-color: #000000;
color: white;
padding: 3px;
border-radius: 5px;
}
}

View File

@@ -1,3 +0,0 @@
.polls-panel {
height: calc(100% - 119px);
}

View File

@@ -1,7 +1,3 @@
.recordingSpinner {
vertical-align: top;
}
.recording-dialog {
flex: 0;
flex-direction: column;
@@ -50,10 +46,6 @@
}
}
.recording-switch-disabled {
opacity: 0.5;
}
.recording-icon-container {
display: inline-flex;
align-items: center;
@@ -156,8 +148,7 @@
*/
font-size: 14px;
.broadcast-dropdown,
.broadcast-dropdown-trigger {
.broadcast-dropdown {
text-align: left;
}

View File

@@ -34,14 +34,6 @@
}
}
.subject-info {
align-items: center;
display: flex;
margin-bottom: 4px;
max-width: 80%;
height: 28px;
}
.details-container {
width: 100%;
display: flex;

View File

@@ -58,21 +58,6 @@
z-index: $toolbarZ;
pointer-events: none;
.button-group-center,
.button-group-left,
.button-group-right {
display: flex;
width: 33%;
}
.button-group-center {
justify-content: center;
}
.button-group-right {
justify-content: flex-end;
}
.toolbox-button-wth-dialog {
display: inline-block;
}
@@ -112,16 +97,6 @@
padding-bottom: env(safe-area-inset-bottom, 0);
}
.beta-tag {
background: #36383C;
border-radius: 3px;
color: #fff;
font-size: 12px;
margin-left: 8px;
padding: 0 4px;
text-transform: uppercase;
}
.overflow-menu-hr {
border-top: 1px solid #4C4D50;
border-bottom: 0;

View File

@@ -3,7 +3,7 @@
}
.hidden {
display: none;
display: none;
}
/**
@@ -24,24 +24,6 @@
display: block !important;
}
/**
* Shows an inline element.
*/
.show-inline {
display: inline-block !important;
}
/**
* Shows a flex element.
*/
.show-flex {
display: -webkit-box !important;
display: -moz-box !important;
display: -ms-flexbox !important;
display: -webkit-flex !important;
display: flex !important;
}
/**
* resets default button styles,
* mostly intended to be used on interactive elements that

View File

@@ -209,23 +209,6 @@
}
}
#reloadPresentation {
display: none;
position: absolute;
color: #FFFFFF;
top: 0;
right:0;
padding: 10px 10px;
font-size: 11pt;
cursor: pointer;
background: rgba(0, 0, 0, 0.3);
border-radius: 5px;
background-clip: padding-box;
-webkit-border-radius: 5px;
-webkit-background-clip: padding-box;
z-index: 20; /*The reload button should appear on top of the header!*/
}
#dominantSpeaker {
visibility: hidden;
width: 300px;
@@ -236,10 +219,6 @@
transform: translateY(-50%);
}
#mixedstream {
display:none !important;
}
#dominantSpeakerAvatarContainer,
.dynamic-shadow {
width: 200px;
@@ -309,11 +288,6 @@
object-fit: cover;
}
.videoMessageFilter {
-webkit-filter: grayscale(.5) opacity(0.8);
filter: grayscale(.5) opacity(0.8);
}
#remotePresenceMessage,
#remoteConnectionMessage {
position: absolute;

View File

@@ -37,7 +37,6 @@ $flagsImagePath: "../images/";
@import 'modals/screen-share/share-audio';
@import 'modals/screen-share/share-screen-warning';
@import 'videolayout_default';
@import 'notice';
@import 'subject';
@import 'popup_menu';
@import 'recording';
@@ -73,12 +72,10 @@ $flagsImagePath: "../images/";
@import 'premeeting/main';
@import 'modals/invite/invite_more';
@import 'modals/security/security';
@import 'e2ee';
@import 'responsive';
@import 'drawer';
@import 'participants-pane';
@import 'reactions-menu';
@import 'plan-limit';
@import 'polls';
/* Modules END */

View File

@@ -14,14 +14,14 @@ PODS:
- CocoaLumberjack/Core (= 3.7.2)
- CocoaLumberjack/Core (3.7.2)
- DoubleConversion (1.1.6)
- FBLazyVector (0.69.10)
- FBReactNativeSpec (0.69.10):
- FBLazyVector (0.69.11)
- FBReactNativeSpec (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.69.10)
- RCTTypeSafety (= 0.69.10)
- React-Core (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- RCTRequired (= 0.69.11)
- RCTTypeSafety (= 0.69.11)
- React-Core (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- Firebase/Analytics (8.15.0):
- Firebase/Core
- Firebase/Core (8.15.0):
@@ -164,203 +164,203 @@ PODS:
- DoubleConversion
- fmt (~> 6.2.1)
- glog
- RCTRequired (0.69.10)
- RCTTypeSafety (0.69.10):
- FBLazyVector (= 0.69.10)
- RCTRequired (= 0.69.10)
- React-Core (= 0.69.10)
- React (0.69.10):
- React-Core (= 0.69.10)
- React-Core/DevSupport (= 0.69.10)
- React-Core/RCTWebSocket (= 0.69.10)
- React-RCTActionSheet (= 0.69.10)
- React-RCTAnimation (= 0.69.10)
- React-RCTBlob (= 0.69.10)
- React-RCTImage (= 0.69.10)
- React-RCTLinking (= 0.69.10)
- React-RCTNetwork (= 0.69.10)
- React-RCTSettings (= 0.69.10)
- React-RCTText (= 0.69.10)
- React-RCTVibration (= 0.69.10)
- React-bridging (0.69.10):
- RCTRequired (0.69.11)
- RCTTypeSafety (0.69.11):
- FBLazyVector (= 0.69.11)
- RCTRequired (= 0.69.11)
- React-Core (= 0.69.11)
- React (0.69.11):
- React-Core (= 0.69.11)
- React-Core/DevSupport (= 0.69.11)
- React-Core/RCTWebSocket (= 0.69.11)
- React-RCTActionSheet (= 0.69.11)
- React-RCTAnimation (= 0.69.11)
- React-RCTBlob (= 0.69.11)
- React-RCTImage (= 0.69.11)
- React-RCTLinking (= 0.69.11)
- React-RCTNetwork (= 0.69.11)
- React-RCTSettings (= 0.69.11)
- React-RCTText (= 0.69.11)
- React-RCTVibration (= 0.69.11)
- React-bridging (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsi (= 0.69.10)
- React-callinvoker (0.69.10)
- React-Codegen (0.69.10):
- FBReactNativeSpec (= 0.69.10)
- React-jsi (= 0.69.11)
- React-callinvoker (0.69.11)
- React-Codegen (0.69.11):
- FBReactNativeSpec (= 0.69.11)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.69.10)
- RCTTypeSafety (= 0.69.10)
- React-Core (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-Core (0.69.10):
- RCTRequired (= 0.69.11)
- RCTTypeSafety (= 0.69.11)
- React-Core (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-Core (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.69.10)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-Core/Default (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/CoreModulesHeaders (0.69.10):
- React-Core/CoreModulesHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/Default (0.69.10):
- React-Core/Default (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/DevSupport (0.69.10):
- React-Core/DevSupport (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.69.10)
- React-Core/RCTWebSocket (= 0.69.10)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-jsinspector (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-Core/Default (= 0.69.11)
- React-Core/RCTWebSocket (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-jsinspector (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTActionSheetHeaders (0.69.10):
- React-Core/RCTActionSheetHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTAnimationHeaders (0.69.10):
- React-Core/RCTAnimationHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTBlobHeaders (0.69.10):
- React-Core/RCTBlobHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTImageHeaders (0.69.10):
- React-Core/RCTImageHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTLinkingHeaders (0.69.10):
- React-Core/RCTLinkingHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTNetworkHeaders (0.69.10):
- React-Core/RCTNetworkHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTSettingsHeaders (0.69.10):
- React-Core/RCTSettingsHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTTextHeaders (0.69.10):
- React-Core/RCTTextHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTVibrationHeaders (0.69.10):
- React-Core/RCTVibrationHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTWebSocket (0.69.10):
- React-Core/RCTWebSocket (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.69.10)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-Core/Default (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-CoreModules (0.69.10):
- React-CoreModules (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/CoreModulesHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- React-RCTImage (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-cxxreact (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/CoreModulesHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- React-RCTImage (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-cxxreact (0.69.11):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-callinvoker (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsinspector (= 0.69.10)
- React-logger (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-runtimeexecutor (= 0.69.10)
- React-jsi (0.69.10):
- React-callinvoker (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsinspector (= 0.69.11)
- React-logger (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-runtimeexecutor (= 0.69.11)
- React-jsi (0.69.11):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsi/Default (= 0.69.10)
- React-jsi/Default (0.69.10):
- React-jsi/Default (= 0.69.11)
- React-jsi/Default (0.69.11):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsiexecutor (0.69.10):
- React-jsiexecutor (0.69.11):
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-jsinspector (0.69.10)
- React-logger (0.69.10):
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-jsinspector (0.69.11)
- React-logger (0.69.11):
- glog
- react-native-background-timer (2.4.1):
- React-Core
@@ -395,72 +395,72 @@ PODS:
- React-Core
- react-native-webview (11.15.1):
- React-Core
- React-perflogger (0.69.10)
- React-RCTActionSheet (0.69.10):
- React-Core/RCTActionSheetHeaders (= 0.69.10)
- React-RCTAnimation (0.69.10):
- React-perflogger (0.69.11)
- React-RCTActionSheet (0.69.11):
- React-Core/RCTActionSheetHeaders (= 0.69.11)
- React-RCTAnimation (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/RCTAnimationHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTBlob (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTAnimationHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTBlob (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- React-Codegen (= 0.69.10)
- React-Core/RCTBlobHeaders (= 0.69.10)
- React-Core/RCTWebSocket (= 0.69.10)
- React-jsi (= 0.69.10)
- React-RCTNetwork (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTImage (0.69.10):
- React-Codegen (= 0.69.11)
- React-Core/RCTBlobHeaders (= 0.69.11)
- React-Core/RCTWebSocket (= 0.69.11)
- React-jsi (= 0.69.11)
- React-RCTNetwork (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTImage (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/RCTImageHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- React-RCTNetwork (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTLinking (0.69.10):
- React-Codegen (= 0.69.10)
- React-Core/RCTLinkingHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTNetwork (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTImageHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- React-RCTNetwork (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTLinking (0.69.11):
- React-Codegen (= 0.69.11)
- React-Core/RCTLinkingHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTNetwork (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/RCTNetworkHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTSettings (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTNetworkHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTSettings (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/RCTSettingsHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTText (0.69.10):
- React-Core/RCTTextHeaders (= 0.69.10)
- React-RCTVibration (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTSettingsHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTText (0.69.11):
- React-Core/RCTTextHeaders (= 0.69.11)
- React-RCTVibration (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- React-Codegen (= 0.69.10)
- React-Core/RCTVibrationHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-runtimeexecutor (0.69.10):
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (0.69.10):
- React-Codegen (= 0.69.11)
- React-Core/RCTVibrationHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-runtimeexecutor (0.69.11):
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (0.69.11):
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-bridging (= 0.69.10)
- React-callinvoker (= 0.69.10)
- React-Core (= 0.69.10)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-logger (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-bridging (= 0.69.11)
- React-callinvoker (= 0.69.11)
- React-Core (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-logger (= 0.69.11)
- React-perflogger (= 0.69.11)
- RNCalendarEvents (2.2.0):
- React
- RNCAsyncStorage (1.17.3):
@@ -706,8 +706,8 @@ SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: a8af91c2b5a0029d12ff6b32e428863d63c48991
FBReactNativeSpec: ec5e878f6452a3de5430e0b2324a4d4ae6ac63f6
FBLazyVector: 5c0975e66853436589eae7542f4b956c7e2ef465
FBReactNativeSpec: bb062293e84c33200005312d1807d8cb94a0d66a
Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d
FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa
FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1
@@ -732,19 +732,19 @@ SPEC CHECKSUMS:
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959
RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a
RCTRequired: 3581db0757e7ff9be10718a56b3d79b6a6bd3bdf
RCTTypeSafety: ce13e630c48340401ebfb28710959913f74b8b36
React: cca8f2b7cce018f79847ca79847fa367b206e8a1
React-bridging: b893643f09d3964afba6c347e00dd86cf10691e5
React-callinvoker: 9ac7cba30428eddf7a06d1253f8e7561b5c97334
React-Codegen: 65ff9fbddf8a17a6d4f495f71d365288f934a93a
React-Core: 550b694774bc778b5c7bf7608fc12a484e01ec05
React-CoreModules: c332d5b416cb3ccf972e7af79d496498a700e073
React-cxxreact: c5c4106bfd2d0cee80b848e33b7ff4e35a721b16
React-jsi: 6ff3fb9b9764a499c959e0096c0d384fa2b4beef
React-jsiexecutor: 388f1c99404c848141d7ea162f61233d04829ede
React-jsinspector: a4463b3411b8b9b37153255ef694a84c77ba3c7f
React-logger: 2a0497622cbabc47fb769d97620952df14c1f814
RCTRequired: 8e9a57dddc8f8e9e816c67c2d2537271a997137a
RCTTypeSafety: 2b19e268e2036a2c2f6db6deb1ac03e28b1d607a
React: f9478e6390f177ee6b67b87a3c6afea42b39523e
React-bridging: d405ecd3ff80e1d0a4059a11063eaa9ed7a00c58
React-callinvoker: c8ffa61f3f06f486ba6647769fc98f19e25d165a
React-Codegen: 73acfdac1495b91ad5efdd3ab005568263c5def6
React-Core: 7b7c75af4b73fe0ed4e5c3cdb7d79979e81148dc
React-CoreModules: cd6e7efb38162884f08c7afa16fffaf15ff28ae4
React-cxxreact: 51157cc600c9f436a7e623913a03b775305ef86c
React-jsi: 3eeb345c4828d7b132fd38064a305f31b46d4ec3
React-jsiexecutor: 5813455a4a908fb7284aa13307a9e0386e93b0bb
React-jsinspector: 9ca5bf73ed0a195397e45fdbcd507cf7d503c428
React-logger: 700340e325f21ba2a2d6413a61ef14268c7360aa
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
react-native-get-random-values: 30b3f74ca34e30e2e480de48e4add2706a40ac8f
react-native-keep-awake: afad8a51dfef9fe9655a6344771be32c8596d774
@@ -757,18 +757,18 @@ SPEC CHECKSUMS:
react-native-video: bb6f12a7198db53b261fefb5d609dc77417acc8b
react-native-webrtc: 4d1669c2ed29767fe70b0169428b4466589ecf8b
react-native-webview: ea4899a1056c782afa96dd082179a66cbebf5504
React-perflogger: bc57c4a953c1ec913b0d984cf4f2b9842a12bde0
React-RCTActionSheet: 3efa3546119a1050f6c34a461b386dd9e36eaf0b
React-RCTAnimation: e58fb9f1adf7b38af329881ea2740f43ffeea854
React-RCTBlob: d2238645553c3ec787324268c0676148d86e6cc4
React-RCTImage: e6d7c9ab978cae99364fcc96b9238fc7740a13da
React-RCTLinking: 329e88ce217dad464ef34b5d0c40b3ceaac6c9ec
React-RCTNetwork: c8967f2382aac31761ddb750fee53fa34cf7a4ee
React-RCTSettings: 8a825b4b5ea58f6713a7c97eea6cc82e9895188b
React-RCTText: ffcaac5c66bc065f2ccf79b6fe34585adb9e589b
React-RCTVibration: 0039c986626b78242401931bb23c803935fae9d1
React-runtimeexecutor: 5ebf1ddaa706bf2986123f22d2cad905443c2c5f
ReactCommon: 65754b8932ea80272714988268bbfb9f303264a5
React-perflogger: fdee2a0c512167ae4c19c4e230ccf6aa66a6aff0
React-RCTActionSheet: 1cf5fef4e372f1c877969710a51bea4bb25e78fe
React-RCTAnimation: 73816e3acd1f5e3f00166fc7eedb34f6b112f734
React-RCTBlob: 6976c838fb14a1daf75d7c8bb23bae9cbbf726bb
React-RCTImage: ab8a7498f215117f32271698591e4bd932dcf812
React-RCTLinking: e8e78aed2744ab9946cc8ba5716b4938c2efb1e0
React-RCTNetwork: 796f5aed4d932655d292bdc6b40f9502dcdb9542
React-RCTSettings: 7e1cd2a384b45c90caf67464572abe3833b9da3b
React-RCTText: fd6162890828f0761e03c59058fa23c3a21b2e10
React-RCTVibration: 302cfd5cc33669d7abdb7ec6790123baba66e62e
React-runtimeexecutor: 59407514818b2afbb1d7507e4e1ac834d24b0fbd
ReactCommon: b8487da74723562d7368dab27135fd182f00a91c
RNCalendarEvents: 7e65eb4a94f53c1744d1e275f7fafcfaa619f7a3
RNCAsyncStorage: 005c0e2f09575360f142d0d1f1f15e4ec575b1af
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
@@ -780,8 +780,8 @@ SPEC CHECKSUMS:
RNSound: 27e8268bdb0a1f191f219a33267f7e0445e8d62f
RNSVG: f3b60aeeaa81960e2e0536c3a9eef50b667ef3a9
RNWatch: dae6c858a2051dbdcfb00b9a86cf4d90400263b4
Yoga: d24d6184b6b85f742536bd93bd07d69d7b9bb4c1
Yoga: 7f5ad94937ba3fc58c151ad1b7bbada2c275b28e
PODFILE CHECKSUM: e3579df5272b8b697c9fdc0e55aa0845b189c4dd
COCOAPODS: 1.11.3
COCOAPODS: 1.12.1

View File

@@ -370,8 +370,6 @@
"permissionCameraRequiredError": "Camera permission is required to participate in conferences with video. Please grant it in Settings",
"permissionErrorTitle": "Permission required",
"permissionMicRequiredError": "Microphone permission is required to participate in conferences with audio. Please grant it in Settings",
"popupError": "Your browser is blocking pop-up windows from this site. Please enable pop-ups in your browser's security settings and try again.",
"popupErrorTitle": "Pop-up blocked",
"readMore": "more",
"recentlyUsedObjects": "Your recently used objects",
"recording": "Recording",
@@ -439,6 +437,7 @@
"token": "token",
"tokenAuthFailed": "Sorry, you're not allowed to join this call.",
"tokenAuthFailedTitle": "Authentication failed",
"tokenAuthUnsupported": "Token URL is not supported.",
"transcribing": "Transcribing",
"unlockRoom": "Remove meeting $t(lockRoomPassword)",
"user": "User",
@@ -674,6 +673,8 @@
"sessionToken": "Session Token",
"start": "Start Recording",
"stop": "Stop Recording",
"stopping": "Stopping Recording",
"wait": "Please wait while we save your recording",
"yes": "Yes"
},
"lockRoomPassword": "password",

View File

@@ -6,6 +6,9 @@ const UI = {};
import Logger from '@jitsi/logger';
import EventEmitter from 'events';
import {
conferenceWillInit
} from '../../react/features/base/conference/actions';
import { isMobileBrowser } from '../../react/features/base/environment/utils';
import { setColorAlpha } from '../../react/features/base/util/helpers';
import { setDocumentUrl } from '../../react/features/etherpad/actions';
@@ -24,14 +27,11 @@ import {
import UIEvents from '../../service/UI/UIEvents';
import EtherpadManager from './etherpad/Etherpad';
import messageHandler from './util/MessageHandler';
import UIUtil from './util/UIUtil';
import VideoLayout from './videolayout/VideoLayout';
const logger = Logger.getLogger(__filename);
UI.messageHandler = messageHandler;
const eventEmitter = new EventEmitter();
UI.eventEmitter = eventEmitter;
@@ -58,30 +58,6 @@ UI.isFullScreen = function() {
return UIUtil.isFullScreen();
};
/**
* Notify user that server has shut down.
*/
UI.notifyGracefulShutdown = function() {
messageHandler.showError({
descriptionKey: 'dialog.gracefulShutdown',
titleKey: 'dialog.serviceUnavailable'
});
};
/**
* Notify user that reservation error happened.
*/
UI.notifyReservationError = function(code, msg) {
messageHandler.showError({
descriptionArguments: {
code,
msg
},
descriptionKey: 'dialog.reservationErrorMsg',
titleKey: 'dialog.reservationError'
});
};
/**
* Initialize conference UI.
*/
@@ -91,18 +67,9 @@ UI.initConference = function() {
/**
* Starts the UI module and initializes all related components.
*
* @returns {boolean} true if the UI is ready and the conference should be
* established, false - otherwise (for example in the case of welcome page)
*/
UI.start = function() {
VideoLayout.initLargeVideo();
// Do not animate the video area on UI start (second argument passed into
// resizeVideoArea) because the animation is not visible anyway. Plus with
// the current dom layout, the quality label is part of the video layout and
// will be seen animating in.
VideoLayout.resizeVideoArea();
APP.store.dispatch(conferenceWillInit());
if (isMobileBrowser()) {
document.body.classList.add('mobile-browser');
@@ -292,40 +259,6 @@ UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
// Used by torture.
UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
/**
* Notify user that connection failed.
* @param {string} stropheErrorMsg raw Strophe error message
*/
UI.notifyConnectionFailed = function(stropheErrorMsg) {
let descriptionKey;
let descriptionArguments;
if (stropheErrorMsg) {
descriptionKey = 'dialog.connectErrorWithMsg';
descriptionArguments = { msg: stropheErrorMsg };
} else {
descriptionKey = 'dialog.connectError';
}
messageHandler.showError({
descriptionArguments,
descriptionKey,
titleKey: 'connection.CONNFAIL'
});
};
/**
* Notify user that maximum users limit has been reached.
*/
UI.notifyMaxUsersLimitReached = function() {
messageHandler.showError({
hideErrorSupportLink: true,
descriptionKey: 'dialog.maxUsersLimitReached',
titleKey: 'dialog.maxUsersLimitReachedTitle'
});
};
UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
VideoLayout.onLastNEndpointsChanged(leavingIds, enteringIds);
};
@@ -337,13 +270,6 @@ UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
*/
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
UI.notifyTokenAuthFailed = function() {
messageHandler.showError({
descriptionKey: 'dialog.tokenAuthFailed',
titleKey: 'dialog.tokenAuthFailedTitle'
});
};
/**
* Update list of available physical devices.
*/

View File

@@ -1,227 +0,0 @@
/* global APP */
import Logger from '@jitsi/logger';
import { openConnection } from '../../../connection';
import {
openAuthDialog,
openLoginDialog } from '../../../react/features/authentication/actions.web';
import {
LoginDialog,
WaitForOwnerDialog
} from '../../../react/features/authentication/components';
import {
getTokenAuthUrl,
isTokenAuthEnabled
} from '../../../react/features/authentication/functions';
import { getReplaceParticipant } from '../../../react/features/base/config/functions';
import { isDialogOpen } from '../../../react/features/base/dialog/functions';
import { setJWT } from '../../../react/features/base/jwt/actions';
import UIUtil from '../util/UIUtil';
import ExternalLoginDialog from './LoginDialog';
let externalAuthWindow;
const logger = Logger.getLogger(__filename);
/**
* Authenticate using external service or just focus
* external auth window if there is one already.
*
* @param {JitsiConference} room
* @param {string} [lockPassword] password to use if the conference is locked
*/
function doExternalAuth(room, lockPassword) {
const config = APP.store.getState()['features/base/config'];
if (externalAuthWindow) {
externalAuthWindow.focus();
return;
}
if (room.isJoined()) {
let getUrl;
if (isTokenAuthEnabled(config)) {
getUrl = Promise.resolve(getTokenAuthUrl(config)(room.getName(), true));
initJWTTokenListener(room);
} else {
getUrl = room.getExternalAuthUrl(true);
}
getUrl.then(url => {
externalAuthWindow = ExternalLoginDialog.showExternalAuthDialog(
url,
() => {
externalAuthWindow = null;
if (!isTokenAuthEnabled(config)) {
room.join(lockPassword);
}
}
);
});
} else if (isTokenAuthEnabled(config)) {
redirectToTokenAuthService(room.getName());
} else {
room.getExternalAuthUrl().then(UIUtil.redirect);
}
}
/**
* Redirect the user to the token authentication service for the login to be
* performed. Once complete it is expected that the service will bring the user
* back with "?jwt={the JWT token}" query parameter added.
* @param {string} [roomName] the name of the conference room.
*/
export function redirectToTokenAuthService(roomName) {
const config = APP.store.getState()['features/base/config'];
// FIXME: This method will not preserve the other URL params that were
// originally passed.
UIUtil.redirect(getTokenAuthUrl(config)(roomName, false));
}
/**
* Initializes 'message' listener that will wait for a JWT token to be received
* from the token authentication service opened in a popup window.
* @param room the name of the conference room.
*/
function initJWTTokenListener(room) {
/**
*
*/
function listener({ data, source }) {
if (externalAuthWindow !== source) {
logger.warn('Ignored message not coming '
+ 'from external authnetication window');
return;
}
let jwt;
if (data && (jwt = data.jwtToken)) {
logger.info('Received JSON Web Token (JWT):', jwt);
APP.store.dispatch(setJWT(jwt));
const roomName = room.getName();
openConnection({
retry: false,
roomName
}).then(connection => {
// Start new connection
const newRoom = connection.initJitsiConference(
roomName, APP.conference._getConferenceOptions());
// Authenticate from the new connection to get
// the session-ID from the focus, which will then be used
// to upgrade current connection's user role
newRoom.room.moderator.authenticate()
.then(() => {
connection.disconnect();
// At this point we'll have session-ID stored in
// the settings. It will be used in the call below
// to upgrade user's role
room.room.moderator.authenticate()
.then(() => {
logger.info('User role upgrade done !');
// eslint-disable-line no-use-before-define
unregister();
})
.catch((err, errCode) => {
logger.error('Authentication failed: ',
err, errCode);
unregister();
});
})
.catch((error, code) => {
unregister();
connection.disconnect();
logger.error(
'Authentication failed on the new connection',
error, code);
});
}, err => {
unregister();
logger.error('Failed to open new connection', err);
});
}
}
/**
*
*/
function unregister() {
window.removeEventListener('message', listener);
}
if (window.addEventListener) {
window.addEventListener('message', listener, false);
}
}
/**
* Authenticate for the conference.
* Uses external service for auth if conference supports that.
* @param {JitsiConference} room
* @param {string} [lockPassword] password to use if the conference is locked
*/
function authenticate(room, lockPassword) {
const config = APP.store.getState()['features/base/config'];
if (isTokenAuthEnabled(config) || room.isExternalAuthEnabled()) {
doExternalAuth(room, lockPassword);
} else {
APP.store.dispatch(openLoginDialog());
}
}
/**
* Notify user that authentication is required to create the conference.
* @param {JitsiConference} room
* @param {string} [lockPassword] password to use if the conference is locked
*/
function requireAuth(room, lockPassword) {
if (isDialogOpen(APP.store, WaitForOwnerDialog) || isDialogOpen(APP.store, LoginDialog)) {
return;
}
APP.store.dispatch(
openAuthDialog(
room.getName(), authenticate.bind(null, room, lockPassword))
);
}
/**
* De-authenticate local user.
*
* @param {JitsiConference} room
* @param {string} [lockPassword] password to use if the conference is locked
* @returns {Promise}
*/
function logout(room) {
return new Promise(resolve => {
room.room.moderator.logout(resolve);
}).then(url => {
// de-authenticate conference on the fly
if (room.isJoined()) {
const replaceParticipant = getReplaceParticipant(APP.store.getState());
room.join(null, replaceParticipant);
}
return url;
});
}
export default {
authenticate,
logout,
requireAuth
};

View File

@@ -1,28 +0,0 @@
/* global APP */
export default {
/**
* Show notification that external auth is required (using provided url).
* @param {string} url - URL to use for external auth.
* @param {function} callback - callback to invoke when auth popup is closed.
* @returns auth dialog
*/
showExternalAuthDialog(url, callback) {
const dialog = APP.UI.messageHandler.openCenteredPopup(
url, 910, 660,
// On closed
callback
);
if (!dialog) {
APP.UI.messageHandler.showWarning({
descriptionKey: 'dialog.popupError',
titleKey: 'dialog.popupErrorTitle'
});
}
return dialog;
}
};

View File

@@ -1,61 +0,0 @@
/* global APP */
import { showErrorNotification, showWarningNotification } from '../../../react/features/notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../react/features/notifications/constants';
const messageHandler = {
/**
* Opens new popup window for given <tt>url</tt> centered over current
* window.
*
* @param url the URL to be displayed in the popup window
* @param w the width of the popup window
* @param h the height of the popup window
* @param onPopupClosed optional callback function called when popup window
* has been closed.
*
* @returns {object} popup window object if opened successfully or undefined
* in case we failed to open it(popup blocked)
*/
// eslint-disable-next-line max-params
openCenteredPopup(url, w, h, onPopupClosed) {
const l = window.screenX + (window.innerWidth / 2) - (w / 2);
const t = window.screenY + (window.innerHeight / 2) - (h / 2);
const popup = window.open(
url, '_blank',
String(`top=${t}, left=${l}, width=${w}, height=${h}`));
if (popup && onPopupClosed) {
const pollTimer = window.setInterval(() => {
if (popup.closed !== false) {
window.clearInterval(pollTimer);
onPopupClosed();
}
}, 200);
}
return popup;
},
/**
* Shows an error dialog to the user.
*
* @param {object} props - The properties to pass to the
* showErrorNotification action.
*/
showError(props) {
APP.store.dispatch(showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.LONG));
},
/**
* Shows a warning dialog to the user.
*
* @param {object} props - The properties to pass to the
* showWarningNotification action.
*/
showWarning(props) {
APP.store.dispatch(showWarningNotification(props, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
};
export default messageHandler;

View File

@@ -31,18 +31,6 @@ const UIUtil = {
return result;
},
/**
* Redirects to a given URL.
*
* @param {string} url - The redirect URL.
* NOTE: Currently used to redirect to 3rd party location for
* authentication. In most cases redirectWithStoredParams action must be
* used instead of this method in order to preserve current URL params.
*/
redirect(url) {
window.location.href = url;
},
/**
* Indicates if we're currently in full screen mode.
*

82
package-lock.json generated
View File

@@ -60,7 +60,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/v1654.0.0+782350e0/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -74,7 +74,7 @@
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.69.10",
"react-native": "0.69.11",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-callstats": "3.73.7",
@@ -102,7 +102,7 @@
"react-native-webrtc": "111.0.3",
"react-native-webview": "11.15.1",
"react-native-youtube-iframe": "2.2.1",
"react-redux": "7.1.0",
"react-redux": "7.2.9",
"react-textarea-autosize": "8.3.0",
"react-window": "1.8.6",
"react-youtube": "10.1.0",
@@ -5661,7 +5661,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dev": true,
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
@@ -5842,7 +5841,6 @@
"version": "7.1.24",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz",
"integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==",
"dev": true,
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
@@ -12762,8 +12760,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1654.0.0+782350e0/lib-jitsi-meet.tgz",
"integrity": "sha512-uGRFpvKj6FSDhVHWvMerNwzaLflUrHQLXiP7Py0ITqV5Ix0d+hTFw9+OgI/JXzdBM3SLOP3GVNQLQfNR45oveA==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -15667,9 +15665,10 @@
}
},
"node_modules/react-native": {
"version": "0.69.10",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.10.tgz",
"integrity": "sha512-zBsJmFsjyx9zC0JDRH6F6hib3BWbIu65EYPsJBQMDHMSGJ9fL7C6ZV49O7Abockn/dbCHhIWpiSyCuda/bhV8g==",
"version": "0.69.11",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.11.tgz",
"integrity": "sha512-lYSzyDicrE5FLag+Vaq1XmPscQFKFqiUCsDMNkgd+wW9139u3hDEnbEWnXfM/kgF3vrKQUfyLMwfkuLV8EQfug==",
"deprecated": "Issues and pull requests filed against this version are not supported. See the React Native release support policy to learn more: https://github.com/reactwg/react-native-releases#releases-support-policy",
"dependencies": {
"@jest/create-cache-key-function": "^27.0.1",
"@react-native-community/cli": "^8.0.4",
@@ -16123,27 +16122,29 @@
}
},
"node_modules/react-redux": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.0.tgz",
"integrity": "sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==",
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"dependencies": {
"@babel/runtime": "^7.4.5",
"hoist-non-react-statics": "^3.3.0",
"invariant": "^2.2.4",
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^16.8.6"
"react-is": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.3",
"redux": "^2.0.0 || ^3.0.0 || ^4.0.0-0"
"react": "^16.8.3 || ^17 || ^18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-redux/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-refresh": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
@@ -23851,7 +23852,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dev": true,
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
@@ -24032,7 +24032,6 @@
"version": "7.1.24",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz",
"integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==",
"dev": true,
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
@@ -29269,8 +29268,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1654.0.0+782350e0/lib-jitsi-meet.tgz",
"integrity": "sha512-uGRFpvKj6FSDhVHWvMerNwzaLflUrHQLXiP7Py0ITqV5Ix0d+hTFw9+OgI/JXzdBM3SLOP3GVNQLQfNR45oveA==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
@@ -31460,9 +31459,9 @@
}
},
"react-native": {
"version": "0.69.10",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.10.tgz",
"integrity": "sha512-zBsJmFsjyx9zC0JDRH6F6hib3BWbIu65EYPsJBQMDHMSGJ9fL7C6ZV49O7Abockn/dbCHhIWpiSyCuda/bhV8g==",
"version": "0.69.11",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.11.tgz",
"integrity": "sha512-lYSzyDicrE5FLag+Vaq1XmPscQFKFqiUCsDMNkgd+wW9139u3hDEnbEWnXfM/kgF3vrKQUfyLMwfkuLV8EQfug==",
"requires": {
"@jest/create-cache-key-function": "^27.0.1",
"@react-native-community/cli": "^8.0.4",
@@ -31776,23 +31775,16 @@
}
},
"react-redux": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.0.tgz",
"integrity": "sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==",
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"requires": {
"@babel/runtime": "^7.4.5",
"hoist-non-react-statics": "^3.3.0",
"invariant": "^2.2.4",
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^16.8.6"
},
"dependencies": {
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}
"react-is": "^17.0.2"
}
},
"react-refresh": {

View File

@@ -65,7 +65,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/v1654.0.0+782350e0/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -79,7 +79,7 @@
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.69.10",
"react-native": "0.69.11",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-callstats": "3.73.7",
@@ -107,7 +107,7 @@
"react-native-webrtc": "111.0.3",
"react-native-webview": "11.15.1",
"react-native-youtube-iframe": "2.2.1",
"react-redux": "7.1.0",
"react-redux": "7.2.9",
"react-textarea-autosize": "8.3.0",
"react-window": "1.8.6",
"react-youtube": "10.1.0",
@@ -179,9 +179,7 @@
"webpack-dev-server": "4.7.3"
},
"overrides": {
"strophe.js@1.5.0": {
"@xmldom/xmldom": "0.8.7"
}
"@xmldom/xmldom": "0.8.7"
},
"engines": {
"node": ">=14.0.0",

View File

@@ -0,0 +1,19 @@
diff --git a/node_modules/react-native-immersive/index.js b/node_modules/react-native-immersive/index.js
index 55dab57..110260b 100644
--- a/node_modules/react-native-immersive/index.js
+++ b/node_modules/react-native-immersive/index.js
@@ -18,7 +18,13 @@ const Immersive = Platform.OS === 'android' ? {
isListenerEnabled = true
RNImmersive.addImmersiveListener()
},
- removeImmersiveListener: (listener) => DeviceEventEmitter.removeListener('@@IMMERSIVE_STATE_CHANGED', listener)
+ removeImmersiveListener: (listener) => {
+ const immersiveListener = DeviceEventEmitter.addListener('@@IMMERSIVE_STATE_CHANGED', listener);
+
+ return () => {
+ immersiveListener.remove();
+ }
+ }
} : {
on: unSupportedError,
off: unSupportedError,

View File

@@ -0,0 +1 @@
*.tgz

View File

@@ -6,7 +6,7 @@ Inside your project, run `npm i @jitsi/react-native-sdk`.<br/><br/>Additionally,
This can be done by running the following script:
```
"update-deps": "node node_modules/@jitsi/react-native-sdk/update_dependencies.js"
node node_modules/@jitsi/react-native-sdk/update_dependencies.js
```
This will check and update all your dependencies.<br/><br/>
@@ -63,24 +63,15 @@ module.exports = (async () => {
SOUNDS_DIR="${PROJECT_DIR}/../node_modules/@jitsi/react-native-sdk/sounds"
cp $SOUNDS_DIR/* ${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/
```
#### Podfile
- At the beginning of your target step add `pod 'ObjectiveDropboxOfficial', :modular_headers => true`
Run `cd ios && pod install && cd ..`
### Android
- In your build.gradle have at least `minSdkVersion = 24`
- TODO: HOW TO ADD COPY SOUNDS STEP
- In `android/app/src/debug/AndroidManifest.xml` and `android/app/src/main/AndroidManifest.xml`, under the `</application>` tag, include
```
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
```
### TODOS
- Ref ConnectionService to not rely on ReactInstanceHolder anymore
- Add Copy Sounds step to build.gradle
- Include copy sounds step in podspec (if possible)
- Add ranges for dependencies
- Add Build_Config for react native to AppInfoModule

View File

@@ -16,11 +16,10 @@ Pod::Spec.new do |s|
s.platform = :ios, '12.4'
s.preserve_paths = 'ios/**/*'
s.source_files = 'ios/**/*.{h,m,swift}'
s.source_files = 'ios/**/*.{h,m}'
s.dependency 'React-Core'
s.dependency 'ObjectiveDropboxOfficial', '6.2.3'
s.dependency 'JitsiWebRTC', '~> 111.0.0'
s.dependency 'react-native-webrtc'
s.dependency 'ObjectiveDropboxOfficial', '6.2.3'
end

View File

@@ -28,7 +28,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/v1652.0.0+90da4884/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -44,7 +44,7 @@
"react-native-tab-view": "3.1.1",
"react-native-url-polyfill": "1.3.0",
"react-native-youtube-iframe": "2.2.1",
"react-redux": "7.1.0",
"react-redux": "7.2.9",
"redux": "4.0.4",
"redux-thunk": "2.4.1",
"unorm": "1.6.0",
@@ -831,11 +831,51 @@
"node": ">=10.13.0"
}
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
},
"node_modules/@types/react": {
"version": "18.2.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.15.tgz",
"integrity": "sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-redux": {
"version": "7.1.25",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
"integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz",
@@ -1300,6 +1340,11 @@
"node": ">=8.0.0"
}
},
"node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/current-executing-script": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/current-executing-script/-/current-executing-script-0.1.3.tgz",
@@ -2398,8 +2443,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1652.0.0+90da4884/lib-jitsi-meet.tgz",
"integrity": "sha512-QLXzAvGwQLpqpQmnbW9wOeScEf3WoBH+osLywzil6FAMsf3URoHR4jFLmkSUAvoxsLbpUSI1ziFFIprbYPnWuQ==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -3023,22 +3068,34 @@
}
},
"node_modules/react-redux": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.0.tgz",
"integrity": "sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==",
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"dependencies": {
"@babel/runtime": "^7.4.5",
"hoist-non-react-statics": "^3.3.0",
"invariant": "^2.2.4",
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^16.8.6"
"react-is": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.3",
"redux": "^2.0.0 || ^3.0.0 || ^4.0.0-0"
"react": "^16.8.3 || ^17 || ^18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-redux/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -4148,11 +4205,51 @@
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
},
"@types/react": {
"version": "18.2.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.15.tgz",
"integrity": "sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/react-redux": {
"version": "7.1.25",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
"integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"@types/scheduler": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
},
"@xmldom/xmldom": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz",
@@ -4480,6 +4577,11 @@
"css-tree": "^1.1.2"
}
},
"csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"current-executing-script": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/current-executing-script/-/current-executing-script-0.1.3.tgz",
@@ -5247,8 +5349,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1652.0.0+90da4884/lib-jitsi-meet.tgz",
"integrity": "sha512-QLXzAvGwQLpqpQmnbW9wOeScEf3WoBH+osLywzil6FAMsf3URoHR4jFLmkSUAvoxsLbpUSI1ziFFIprbYPnWuQ==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
@@ -5705,16 +5807,23 @@
}
},
"react-redux": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.0.tgz",
"integrity": "sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==",
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"requires": {
"@babel/runtime": "^7.4.5",
"hoist-non-react-statics": "^3.3.0",
"invariant": "^2.2.4",
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^16.8.6"
"react-is": "^17.0.2"
},
"dependencies": {
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}
}
},
"readdirp": {

View File

@@ -29,7 +29,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/v1652.0.0+90da4884/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -45,7 +45,7 @@
"react-native-tab-view": "3.1.1",
"react-native-url-polyfill": "1.3.0",
"react-native-youtube-iframe": "2.2.1",
"react-redux": "7.1.0",
"react-redux": "7.2.9",
"redux": "4.0.4",
"redux-thunk": "2.4.1",
"unorm": "1.6.0",
@@ -86,9 +86,7 @@
"react-native-webview": "11.15.1"
},
"overrides": {
"strophe.js@1.5.0": {
"@xmldom/xmldom": "0.8.7"
}
"@xmldom/xmldom": "0.8.7"
},
"scripts": {
"postinstall": "node sdk_instructions.js",

View File

@@ -107,10 +107,6 @@ copyFolderRecursiveSync(
`${iosSrcPath}/dropbox`,
iosDestPath
);
copyFolderRecursiveSync(
'../ios/sdk/src/picture-in-picture',
iosDestPath
);
fs.copyFileSync(
`${iosSrcPath}/AppInfo.m`,
`${iosDestPath}/AppInfo.m`

View File

@@ -23,18 +23,24 @@ function updateDependencies() {
}
}
packageJSON.overrides = packageJSON.overrides || {};
for (const key in RNSDKpackageJSON.overrides) {
if (!packageJSON.overrides.hasOwnProperty(key)) {
packageJSON.overrides[key] = RNSDKpackageJSON.overrides[key];
updated = true;
}
}
if (!updated) {
return;
}
console.log(`
=========================
The following dependencies were added to your package.json:
\n
${Object.keys(packageJSON.dependencies)}
\n
Make sure you run npm install
If you are building for ios run cd ios && pod install to link them.
🚀 Your project was updated!
🛠 Make sure you run npm install
📱 If you are building for iOS run cd ios && pod install to link them.
=========================
`);
@@ -46,9 +52,7 @@ If you are building for ios run cd ios && pod install to link them.
return item;
}, {});
const data = JSON.stringify(packageJSON, null, 2);
fs.writeFileSync(pathToPackageJSON, data);
fs.writeFileSync(pathToPackageJSON, JSON.stringify(packageJSON, null, 2));
console.log(
'All needed dependencies have been updated. \nPlease run npm install.'

View File

@@ -8,10 +8,10 @@ import {
import {
createFakeConfig,
restoreConfig
} from '../base/config/functions';
import { connect, disconnect, setLocationURL } from '../base/connection/actions';
} from '../base/config/functions.native';
import { connect, disconnect, setLocationURL } from '../base/connection/actions.native';
import { loadConfig } from '../base/lib-jitsi-meet/functions.native';
import { createDesiredLocalTracks } from '../base/tracks/actions';
import { createDesiredLocalTracks } from '../base/tracks/actions.native';
import isInsecureRoomName from '../base/util/isInsecureRoomName';
import { parseURLParams } from '../base/util/parseURLParams';
import {
@@ -27,6 +27,7 @@ import {
} from '../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../mobile/navigation/routes';
import { clearNotifications } from '../notifications/actions';
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions';
import { addTrackStateToURL, getDefaultURL } from './functions.native';
import logger from './logger';
@@ -137,7 +138,7 @@ export function appNavigate(uri?: string, options: IReloadNowOptions = {}) {
dispatch(setRoom(room));
if (room) {
if (isInsecureRoomName(room)) {
if (isUnsafeRoomWarningEnabled(getState()) && isInsecureRoomName(room)) {
navigateRoot(screen.unsafeRoomWarning);
return;

View File

@@ -1,4 +1,5 @@
import '../analytics/middleware';
import '../authentication/middleware';
import '../av-moderation/middleware';
import '../base/conference/middleware';
import '../base/config/middleware';

View File

@@ -1,4 +1,3 @@
import '../authentication/middleware';
import '../dynamic-branding/middleware';
import '../gifs/middleware';
import '../mobile/audio-mode/middleware';

View File

@@ -1,4 +1,4 @@
import '../authentication/middleware';
import '../base/connection/middleware';
import '../base/i18n/middleware';
import '../base/devices/middleware';
import '../base/media/middleware';

View File

@@ -8,6 +8,24 @@
*/
export const CANCEL_LOGIN = 'CANCEL_LOGIN';
/**
* The type of (redux) action which signals to login.
*
* {
* type: LOGOUT
* }
*/
export const LOGIN = 'LOGIN';
/**
* The type of (redux) action which signals to logout.
*
* {
* type: LOGOUT
* }
*/
export const LOGOUT = 'LOGOUT';
/**
* The type of (redux) action which signals that the cyclic operation of waiting
* for conference owner has been aborted.

View File

@@ -1,7 +1,7 @@
import { IStore } from '../app/types';
import { checkIfCanJoin } from '../base/conference/actions';
import { IJitsiConference } from '../base/conference/reducer';
import { openDialog } from '../base/dialog/actions';
import { hideDialog, openDialog } from '../base/dialog/actions';
import {
STOP_WAIT_FOR_OWNER,
@@ -126,6 +126,16 @@ function _upgradeRoleStarted(thenableWithCancel: Object) {
};
}
/**
* Hides an authentication dialog where the local participant
* should authenticate.
*
* @returns {Function}.
*/
export function hideLoginDialog() {
return hideDialog(LoginDialog);
}
/**
* Opens {@link WaitForOnwerDialog}.
*

View File

@@ -1,4 +1,4 @@
import { appNavigate } from '../app/actions';
import { appNavigate } from '../app/actions.native';
import { IStore } from '../app/types';
import { conferenceLeft } from '../base/conference/actions';
import { connectionFailed } from '../base/connection/actions.native';
@@ -61,4 +61,11 @@ export function cancelWaitForOwner() {
};
}
/** .
* Redirect to the default location (e.g. Welcome page).
*
* @returns {Function}
*/
export function redirectToDefaultLocation() {
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
}

View File

@@ -1,12 +1,11 @@
import { maybeRedirectToWelcomePage } from '../app/actions.web';
import { IStore } from '../app/types';
import { hideDialog, openDialog } from '../base/dialog/actions';
import {
CANCEL_LOGIN
CANCEL_LOGIN,
LOGIN,
LOGOUT
} from './actionTypes';
import LoginDialog from './components/web/LoginDialog';
import WaitForOwnerDialog from './components/web/WaitForOwnerDialog';
export * from './actions.any';
@@ -35,31 +34,37 @@ export function cancelWaitForOwner() {
};
}
/**
* Hides a authentication dialog where the local participant
* should authenticate.
/** .
* Redirect to the default location (e.g. Welcome page).
*
* @returns {Function}.
* @returns {Function}
*/
export function hideLoginDialog() {
return hideDialog(LoginDialog);
export function redirectToDefaultLocation() {
return (dispatch: IStore['dispatch']) => dispatch(maybeRedirectToWelcomePage());
}
/**
* Shows a notification dialog that authentication is required to create the.
* Conference.
* This is used for external auth.
* Login.
*
* @param {string} room - The room name.
* @param {Function} onAuthNow - The function to be invoked when external authentication.
*
* @returns {Function}.
* @returns {{
* type: LOGIN
* }}
*/
export function openAuthDialog(room: String, onAuthNow?: Function) {
return openDialog(WaitForOwnerDialog, {
room,
onAuthNow
});
export function login() {
return {
type: LOGIN
};
}
/**
* Logout.
*
* @returns {{
* type: LOGOUT
* }}
*/
export function logout() {
return {
type: LOGOUT
};
}

View File

@@ -2,11 +2,10 @@ import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect as reduxConnect } from 'react-redux';
// @ts-expect-error
import { connect } from '../../../../../connection';
import { IReduxState, IStore } from '../../../app/types';
import { IJitsiConference } from '../../../base/conference/reducer';
import { IConfig } from '../../../base/config/configType';
import { connect } from '../../../base/connection/actions.web';
import { toJid } from '../../../base/connection/functions';
import { translate, translateToHTML } from '../../../base/i18n/functions';
import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet';
@@ -54,11 +53,6 @@ interface IProps extends WithTranslation {
*/
dispatch: IStore['dispatch'];
/**
* Invoked when username and password are submitted.
*/
onSuccess: Function;
/**
* Conference room name.
*/
@@ -70,11 +64,6 @@ interface IProps extends WithTranslation {
*/
interface IState {
/**
* Authentication process starts before joining the conference room.
*/
loginStarted: boolean;
/**
* The user entered password for the conference.
*/
@@ -102,8 +91,7 @@ class LoginDialog extends Component<IProps, IState> {
this.state = {
username: '',
password: '',
loginStarted: false
password: ''
};
this._onCancelLogin = this._onCancelLogin.bind(this);
@@ -135,8 +123,6 @@ class LoginDialog extends Component<IProps, IState> {
const {
_conference: conference,
_configHosts: configHosts,
roomName,
onSuccess,
dispatch
} = this.props;
const { password, username } = this.state;
@@ -148,19 +134,7 @@ class LoginDialog extends Component<IProps, IState> {
if (conference) {
dispatch(authenticateAndUpgradeRole(jid, password, conference));
} else {
this.setState({
loginStarted: true
});
connect(jid, password, roomName)
.then((connection: any) => {
onSuccess?.(connection);
})
.catch(() => {
this.setState({
loginStarted: false
});
});
dispatch(connect(jid, password));
}
}
@@ -249,7 +223,7 @@ class LoginDialog extends Component<IProps, IState> {
_connecting: connecting,
t
} = this.props;
const { password, loginStarted, username } = this.state;
const { password, username } = this.state;
return (
<Dialog
@@ -258,7 +232,6 @@ class LoginDialog extends Component<IProps, IState> {
hideCloseButton = { true }
ok = {{
disabled: connecting
|| loginStarted
|| !password
|| !username,
translationKey: 'dialog.login'
@@ -315,7 +288,7 @@ function mapStateToProps(state: IReduxState) {
return {
_conference: authRequired || conference,
_configHosts: configHosts,
_connecting: connecting || thenableWithCancel,
_connecting: Boolean(connecting) || Boolean(thenableWithCancel),
_error: connectionError || authenticateAndUpgradeRoleError,
_progress: progress
};

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { IStore } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import Dialog from '../../../base/ui/components/web/Dialog';
import { cancelWaitForOwner } from '../../actions.web';
import { cancelWaitForOwner, login } from '../../actions.web';
/**
* The type of the React {@code Component} props of {@link WaitForOwnerDialog}.
@@ -16,11 +16,6 @@ interface IProps extends WithTranslation {
* Redux store dispatch method.
*/
dispatch: IStore['dispatch'];
/**
* Function to be invoked after click.
*/
onAuthNow?: Function;
}
/**
@@ -61,9 +56,7 @@ class WaitForOwnerDialog extends PureComponent<IProps> {
* @returns {void}
*/
_onIAmHost() {
const { onAuthNow } = this.props;
onAuthNow?.();
this.props.dispatch(login());
}
/**

View File

@@ -1,4 +1,3 @@
import { appNavigate } from '../app/actions.native';
import { IStore } from '../app/types';
import {
CONFERENCE_FAILED,
@@ -6,6 +5,7 @@ import {
CONFERENCE_LEFT
} from '../base/conference/actionTypes';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection/actionTypes';
import { hangup } from '../base/connection/actions';
import { hideDialog } from '../base/dialog/actions';
import { isDialogOpen } from '../base/dialog/functions';
import {
@@ -13,19 +13,28 @@ import {
JitsiConnectionErrors
} from '../base/lib-jitsi-meet';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { getBackendSafeRoomName } from '../base/util/uri';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { openLogoutDialog } from '../settings/actions';
import {
CANCEL_LOGIN,
LOGIN,
LOGOUT,
STOP_WAIT_FOR_OWNER,
UPGRADE_ROLE_FINISHED,
WAIT_FOR_OWNER
} from './actionTypes';
import {
hideLoginDialog,
openLoginDialog,
openWaitForOwnerDialog,
redirectToDefaultLocation,
stopWaitForOwner,
waitForOwner } from './actions.native';
waitForOwner } from './actions';
import { LoginDialog, WaitForOwnerDialog } from './components';
import { getTokenAuthUrl, isTokenAuthEnabled } from './functions';
/**
* Middleware that captures connection or conference failed errors and controls
@@ -40,7 +49,8 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CANCEL_LOGIN: {
const { dispatch, getState } = store;
const { thenableWithCancel } = getState()['features/authentication'];
const state = getState();
const { thenableWithCancel } = state['features/authentication'];
thenableWithCancel?.cancel();
@@ -57,10 +67,8 @@ MiddlewareRegistry.register(store => next => action => {
return result;
}
// Go back to the app's entry point.
_hideLoginDialog(store);
dispatch(hideLoginDialog());
const state = getState();
const { authRequired, conference } = state['features/base/conference'];
const { passwordRequired } = state['features/base/connection'];
@@ -68,7 +76,7 @@ MiddlewareRegistry.register(store => next => action => {
// NOTE: Despite it's confusing name, `passwordRequired` implies an XMPP
// connection auth error.
if ((passwordRequired || authRequired) && !conference) {
dispatch(appNavigate(undefined));
dispatch(redirectToDefaultLocation());
}
}
break;
@@ -100,7 +108,7 @@ MiddlewareRegistry.register(store => next => action => {
if (_isWaitingForOwner(store)) {
store.dispatch(stopWaitForOwner());
}
_hideLoginDialog(store);
store.dispatch(hideLoginDialog());
break;
case CONFERENCE_LEFT:
@@ -108,18 +116,43 @@ MiddlewareRegistry.register(store => next => action => {
break;
case CONNECTION_ESTABLISHED:
_hideLoginDialog(store);
store.dispatch(hideLoginDialog());
break;
case CONNECTION_FAILED: {
const { error } = action;
const state = store.getState();
const { jwt } = state['features/base/jwt'];
if (error
&& error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
&& typeof error.recoverable === 'undefined') {
&& typeof error.recoverable === 'undefined'
&& !jwt) {
error.recoverable = true;
store.dispatch(openLoginDialog());
_handleLogin(store);
}
break;
}
case LOGIN: {
_handleLogin(store);
break;
}
case LOGOUT: {
const { conference } = store.getState()['features/base/conference'];
if (!conference) {
break;
}
store.dispatch(openLogoutDialog(() =>
conference.room.moderator.logout(() => store.dispatch(hangup(true)))
));
break;
}
@@ -132,7 +165,7 @@ MiddlewareRegistry.register(store => next => action => {
const { error, progress } = action;
if (!error && progress === 1) {
_hideLoginDialog(store);
store.dispatch(hideLoginDialog());
}
break;
}
@@ -144,7 +177,7 @@ MiddlewareRegistry.register(store => next => action => {
action.waitForOwnerTimeoutID = setTimeout(handler, timeoutMs);
// The WAIT_FOR_OWNER action is cyclic and we don't want to hide the
// The WAIT_FOR_OWNER action is cyclic, and we don't want to hide the
// login dialog every few seconds.
isDialogOpen(store, LoginDialog)
|| store.dispatch(openWaitForOwnerDialog());
@@ -162,22 +195,12 @@ MiddlewareRegistry.register(store => next => action => {
* @param {Object} store - The redux store.
* @returns {void}
*/
function _clearExistingWaitForOwnerTimeout(
{ getState }: IStore) {
function _clearExistingWaitForOwnerTimeout({ getState }: IStore) {
const { waitForOwnerTimeoutID } = getState()['features/authentication'];
waitForOwnerTimeoutID && clearTimeout(waitForOwnerTimeoutID);
}
/**
* Hides {@link LoginDialog} if it's currently displayed.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function _hideLoginDialog({ dispatch }: IStore) {
dispatch(hideDialog(LoginDialog));
}
/**
* Checks if the cyclic "wait for conference owner" task is currently scheduled.
@@ -188,3 +211,35 @@ function _hideLoginDialog({ dispatch }: IStore) {
function _isWaitingForOwner({ getState }: IStore) {
return Boolean(getState()['features/authentication'].waitForOwnerTimeoutID);
}
/**
* Handles login challenge. Opens login dialog or redirects to token auth URL.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @returns {void}
*/
function _handleLogin({ dispatch, getState }: IStore) {
const state = getState();
const config = state['features/base/config'];
const room = getBackendSafeRoomName(state['features/base/conference'].room);
if (isTokenAuthEnabled(config)) {
if (typeof APP === 'undefined') {
dispatch(showErrorNotification({
descriptionKey: 'dialog.tokenAuthUnsupported',
titleKey: 'dialog.tokenAuthFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
dispatch(redirectToDefaultLocation());
return;
}
// FIXME: This method will not preserve the other URL params that were originally passed.
// redirectToTokenAuthService
window.location.href = getTokenAuthUrl(config)(room, false);
} else {
dispatch(openLoginDialog());
}
}

View File

@@ -1,152 +0,0 @@
import { maybeRedirectToWelcomePage } from '../app/actions.web';
import { IStore } from '../app/types';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../base/conference/actionTypes';
import { CONNECTION_ESTABLISHED } from '../base/connection/actionTypes';
import { hideDialog } from '../base/dialog/actions';
import { isDialogOpen } from '../base/dialog/functions';
import {
JitsiConferenceErrors
} from '../base/lib-jitsi-meet';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import {
CANCEL_LOGIN,
STOP_WAIT_FOR_OWNER,
UPGRADE_ROLE_FINISHED,
WAIT_FOR_OWNER
} from './actionTypes';
import {
hideLoginDialog,
openWaitForOwnerDialog,
stopWaitForOwner
} from './actions.web';
import LoginDialog from './components/web/LoginDialog';
import WaitForOwnerDialog from './components/web/WaitForOwnerDialog';
/**
* Middleware that captures connection or conference failed errors and controls
* {@link WaitForOwnerDialog} and {@link LoginDialog}.
*
* FIXME Some of the complexity was introduced by the lack of dialog stacking.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CANCEL_LOGIN: {
const { dispatch, getState } = store;
if (!isDialogOpen(store, WaitForOwnerDialog)) {
if (_isWaitingForOwner(store)) {
dispatch(openWaitForOwnerDialog());
return next(action);
}
dispatch(hideLoginDialog());
const { authRequired, conference } = getState()['features/base/conference'];
const { passwordRequired } = getState()['features/base/connection'];
// Only end the meeting if we are not already inside and trying to upgrade.
if ((authRequired && !conference) || passwordRequired) {
dispatch(maybeRedirectToWelcomePage());
}
}
break;
}
case CONFERENCE_FAILED: {
const { error } = action;
let recoverable;
if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) {
if (typeof error.recoverable === 'undefined') {
error.recoverable = true;
}
recoverable = error.recoverable;
}
if (recoverable) {
// we haven't migrated all the code from AuthHandler, and we need for now conference.js to trigger
// the dialog to pass all required parameters to WaitForOwnerDialog
// keep it commented, so we do not trigger sending iqs to jicofo twice
// and showing the broken dialog with no handler
// store.dispatch(waitForOwner());
} else {
store.dispatch(stopWaitForOwner());
}
break;
}
case CONFERENCE_JOINED:
store.dispatch(stopWaitForOwner());
store.dispatch(hideLoginDialog());
break;
case CONFERENCE_LEFT:
store.dispatch(stopWaitForOwner());
break;
case CONNECTION_ESTABLISHED:
store.dispatch(hideLoginDialog());
break;
case STOP_WAIT_FOR_OWNER:
_clearExistingWaitForOwnerTimeout(store);
store.dispatch(hideDialog(WaitForOwnerDialog));
break;
case UPGRADE_ROLE_FINISHED: {
const { error, progress } = action;
if (!error && progress === 1) {
store.dispatch(hideLoginDialog());
}
break;
}
case WAIT_FOR_OWNER: {
_clearExistingWaitForOwnerTimeout(store);
const { handler, timeoutMs }: { handler: () => void; timeoutMs: number; } = action;
action.waitForOwnerTimeoutID = setTimeout(handler, timeoutMs);
isDialogOpen(store, LoginDialog)
|| store.dispatch(openWaitForOwnerDialog());
break;
}
}
return next(action);
});
/**
* Will clear the wait for conference owner timeout handler if any is currently
* set.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function _clearExistingWaitForOwnerTimeout(
{ getState }: IStore) {
const { waitForOwnerTimeoutID } = getState()['features/authentication'];
waitForOwnerTimeoutID && clearTimeout(waitForOwnerTimeoutID);
}
/**
* Checks if the cyclic "wait for conference owner" task is currently scheduled.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function _isWaitingForOwner({ getState }: IStore) {
return getState()['features/authentication'].waitForOwnerTimeoutID;
}

View File

@@ -106,6 +106,15 @@ export const CONFERENCE_UNIQUE_ID_SET = 'CONFERENCE_UNIQUE_ID_SET';
*/
export const E2E_RTT_CHANGED = 'E2E_RTT_CHANGED'
/**
* The type of (redux) action which signals that a conference will be initialized.
*
* {
* type: CONFERENCE_WILL_INIT
* }
*/
export const CONFERENCE_WILL_INIT = 'CONFERENCE_WILL_INIT';
/**
* The type of (redux) action which signals that a specific conference will be
* joined.

View File

@@ -1,11 +1,10 @@
import { createStartMutedConfigurationEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { appNavigate } from '../../app/actions';
import { IReduxState, IStore } from '../../app/types';
import { endpointMessageReceived } from '../../subtitles/actions.any';
import { iAmVisitor } from '../../visitors/functions';
import { getReplaceParticipant } from '../config/functions';
import { disconnect } from '../connection/actions';
import { hangup } from '../connection/actions';
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants';
import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../media/actions';
@@ -41,6 +40,7 @@ import {
CONFERENCE_SUBJECT_CHANGED,
CONFERENCE_TIMESTAMP_CHANGED,
CONFERENCE_UNIQUE_ID_SET,
CONFERENCE_WILL_INIT,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_CLOSED,
@@ -463,6 +463,19 @@ export function _conferenceWillJoin(conference: IJitsiConference) {
};
}
/**
* Signals the intention of the application to have a conference initialized.
*
* @returns {{
* type: CONFERENCE_WILL_INIT
* }}
*/
export function conferenceWillInit() {
return {
type: CONFERENCE_WILL_INIT
};
}
/**
* Signals the intention of the application to have the local participant
* join the specified conference.
@@ -647,18 +660,9 @@ export function kickedOut(conference: IJitsiConference, participant: Object) {
* @returns {Function}
*/
export function leaveConference() {
return async (dispatch: IStore['dispatch']) => {
// FIXME: these should be unified.
if (navigator.product === 'ReactNative') {
dispatch(appNavigate(undefined));
} else {
dispatch(disconnect(true));
}
};
return async (dispatch: IStore['dispatch']) => dispatch(hangup(true));
}
/**
* Signals that the lock state of a specific JitsiConference changed.
*

View File

@@ -16,6 +16,8 @@ import { openDisplayNamePrompt } from '../../display-name/actions';
import { readyToClose } from '../../mobile/external-api/actions';
import { showErrorNotification, showWarningNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { stopLocalVideoRecording } from '../../recording/actions.any';
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager';
import { setIAmVisitor } from '../../visitors/actions';
import { iAmVisitor } from '../../visitors/functions';
import { overwriteConfig } from '../config/actions';
@@ -47,6 +49,7 @@ import {
} from './actionTypes';
import {
conferenceFailed,
conferenceWillInit,
conferenceWillLeave,
createConference,
leaveConference,
@@ -71,7 +74,7 @@ import logger from './logger';
/**
* Handler for before unload event.
*/
let beforeUnloadHandler: (() => void) | undefined;
let beforeUnloadHandler: ((e?: any) => void) | undefined;
/**
* Implements the middleware of the feature base/conference.
@@ -142,12 +145,6 @@ MiddlewareRegistry.register(store => next => action => {
function _conferenceFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
const { conference, error } = action;
if (error.name === JitsiConferenceErrors.REDIRECTED) {
if (typeof error.recoverable === 'undefined') {
error.recoverable = true;
}
}
const result = next(action);
const { enableForcedReload } = getState()['features/base/config'];
@@ -195,18 +192,22 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
break;
}
case JitsiConferenceErrors.CONFERENCE_MAX_USERS: {
if (typeof APP === 'undefined') {
// in case of max users(it can be from a visitor node), let's restore
// oldConfig if any as we will be back to the main prosody
const newConfig = restoreConferenceOptions(getState);
dispatch(showErrorNotification({
hideErrorSupportLink: true,
descriptionKey: 'dialog.maxUsersLimitReached',
titleKey: 'dialog.maxUsersLimitReachedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
if (newConfig) {
dispatch(overwriteConfig(newConfig)) // @ts-ignore
.then(dispatch(conferenceWillLeave(conference)))
.then(conference.leave())
.then(dispatch(disconnect()))
.then(dispatch(connect()));
}
// In case of max users(it can be from a visitor node), let's restore
// oldConfig if any as we will be back to the main prosody.
const newConfig = restoreConferenceOptions(getState);
if (newConfig) {
dispatch(overwriteConfig(newConfig)) // @ts-ignore
.then(dispatch(conferenceWillLeave(conference)))
.then(conference.leave())
.then(dispatch(disconnect()))
.then(dispatch(connect()));
}
break;
@@ -214,45 +215,48 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
sendAnalytics(createOfferAnswerFailedEvent());
break;
case JitsiConferenceErrors.REDIRECTED: {
// once conference.js is gone this can be removed and both
// redirect logics to be merged
if (typeof APP === 'undefined') {
const newConfig = getVisitorOptions(getState, error.params);
const newConfig = getVisitorOptions(getState, error.params);
if (!newConfig) {
logger.warn('Not redirected missing params');
break;
}
const [ vnode ] = error.params;
dispatch(overwriteConfig(newConfig)) // @ts-ignore
.then(dispatch(conferenceWillLeave(conference)))
.then(conference.leave())
.then(dispatch(disconnect()))
.then(dispatch(setIAmVisitor(Boolean(vnode))))
// we do not clear local tracks on error, so we need to manually clear them
.then(dispatch(destroyLocalTracks()))
.then(dispatch(connect()));
if (!newConfig) {
logger.warn('Not redirected missing params');
break;
}
const [ vnode ] = error.params;
dispatch(overwriteConfig(newConfig)) // @ts-ignore
.then(() => dispatch(conferenceWillLeave(conference)))
.then(() => dispatch(disconnect()))
.then(() => dispatch(setIAmVisitor(Boolean(vnode))))
// we do not clear local tracks on error, so we need to manually clear them
.then(() => dispatch(destroyLocalTracks()))
.then(() => dispatch(conferenceWillInit()))
.then(() => dispatch(connect()))
.then(() => {
// FIXME: Workaround for the web version. To be removed once we get rid of conference.js
if (typeof APP !== 'undefined') {
APP.conference.startConference([]);
}
});
break;
}
}
if (typeof APP === 'undefined') {
!error.recoverable
&& conference
&& conference.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).catch((reason: Error) => {
// Even though we don't care too much about the failure, it may be
// good to know that it happen, so log it (on the info level).
logger.info('JitsiConference.leave() rejected with:', reason);
});
} else {
// FIXME: Workaround for the web version. Currently, the creation of the
// conference is handled by /conference.js and appropriate failure handlers
// are set there.
!error.recoverable
&& conference
&& conference.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).catch((reason: Error) => {
// Even though we don't care too much about the failure, it may be
// good to know that it happen, so log it (on the info level).
logger.info('JitsiConference.leave() rejected with:', reason);
});
// FIXME: Workaround for the web version. Currently, the creation of the
// conference is handled by /conference.js and appropriate failure handlers
// are set there.
if (typeof APP !== 'undefined') {
_removeUnloadHandler(getState);
}
@@ -295,7 +299,14 @@ function _conferenceJoined({ dispatch, getState }: IStore, next: Function, actio
// handles the process of leaving the conference. This is temporary solution
// that should cover the described use case as part of the effort to
// implement the conferenceWillLeave action for web.
beforeUnloadHandler = () => {
beforeUnloadHandler = (e?: any) => {
if (LocalRecordingManager.isRecordingLocally()) {
dispatch(stopLocalVideoRecording());
if (e) {
e.preventDefault();
e.returnValue = null;
}
}
dispatch(conferenceWillLeave(conference));
};
@@ -330,12 +341,16 @@ function _conferenceJoined({ dispatch, getState }: IStore, next: Function, actio
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _connectionEstablished({ dispatch }: IStore, next: Function, action: AnyAction) {
async function _connectionEstablished({ dispatch }: IStore, next: Function, action: AnyAction) {
const result = next(action);
// FIXME: Workaround for the web version. Currently, the creation of the
// conference is handled by /conference.js.
typeof APP === 'undefined' && dispatch(createConference());
if (typeof APP === 'undefined') {
dispatch(createConference());
return result;
}
return result;
}
@@ -377,45 +392,45 @@ function _logJwtErrors(message: string, state: IReduxState) {
function _connectionFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
_logJwtErrors(action.error.message, getState());
dispatch(showErrorNotification({
descriptionKey: 'dialog.tokenAuthFailed',
titleKey: 'dialog.tokenAuthFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
const result = next(action);
_removeUnloadHandler(getState);
// FIXME: Workaround for the web version. Currently, the creation of the
// conference is handled by /conference.js and appropriate failure handlers
// are set there.
if (typeof APP === 'undefined') {
const { connection } = action;
const { error } = action;
const { connection } = action;
const { error } = action;
forEachConference(getState, conference => {
// It feels that it would make things easier if JitsiConference
// in lib-jitsi-meet would monitor it's connection and emit
// CONFERENCE_FAILED when it's dropped. It has more knowledge on
// whether it can recover or not. But because the reload screen
// and the retry logic is implemented in the app maybe it can be
// left this way for now.
if (conference.getConnection() === connection) {
// XXX Note that on mobile the error type passed to
// connectionFailed is always an object with .name property.
// This fact needs to be checked prior to enabling this logic on
// web.
const conferenceAction
= conferenceFailed(conference, error.name);
forEachConference(getState, conference => {
// TODO: revisit this
// It feels that it would make things easier if JitsiConference
// in lib-jitsi-meet would monitor it's connection and emit
// CONFERENCE_FAILED when it's dropped. It has more knowledge on
// whether it can recover or not. But because the reload screen
// and the retry logic is implemented in the app maybe it can be
// left this way for now.
if (conference.getConnection() === connection) {
// XXX Note that on mobile the error type passed to
// connectionFailed is always an object with .name property.
// This fact needs to be checked prior to enabling this logic on
// web.
const conferenceAction = conferenceFailed(conference, error.name);
// Copy the recoverable flag if set on the CONNECTION_FAILED
// action to not emit recoverable action caused by
// a non-recoverable one.
if (typeof error.recoverable !== 'undefined') {
conferenceAction.error.recoverable = error.recoverable;
}
dispatch(conferenceAction);
// Copy the recoverable flag if set on the CONNECTION_FAILED
// action to not emit recoverable action caused by
// a non-recoverable one.
if (typeof error.recoverable !== 'undefined') {
conferenceAction.error.recoverable = error.recoverable;
}
return true;
});
}
dispatch(conferenceAction);
}
return true;
});
return result;
}

View File

@@ -9,7 +9,8 @@ import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_JOIN_IN_PROGRESS,
CONFERENCE_LEFT, KICKED_OUT
CONFERENCE_LEFT,
KICKED_OUT
} from './actionTypes';
import logger from './logger';
import './middleware.any';

View File

@@ -155,6 +155,9 @@ export interface IConferenceState {
export interface IJitsiConferenceRoom {
locked: boolean;
moderator: {
logout: Function;
};
myroomjid: string;
roomjid: string;
}

View File

@@ -1,6 +1,10 @@
import _ from 'lodash';
import { IReduxState } from '../../app/types';
import { IReduxState, IStore } from '../../app/types';
import { setPrejoinDisplayNameRequired } from '../../prejoin/actions.any';
import { conferenceLeft, conferenceWillLeave } from '../conference/actions';
import { getCurrentConference } from '../conference/functions';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import {
appendURLParam,
getBackendSafeRoomName
@@ -10,8 +14,10 @@ import {
CONNECTION_DISCONNECTED,
CONNECTION_ESTABLISHED,
CONNECTION_FAILED,
CONNECTION_WILL_CONNECT,
SET_LOCATION_URL
} from './actionTypes';
import { JITSI_CONNECTION_URL_KEY } from './constants';
import logger from './logger';
/**
@@ -194,3 +200,194 @@ export function setLocationURL(locationURL?: URL) {
locationURL
};
}
/**
* Opens new connection.
*
* @param {string} [id] - The XMPP user's ID (e.g. {@code user@server.com}).
* @param {string} [password] - The XMPP user's password.
* @returns {Function}
*/
export function _connectInternal(id?: string, password?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const options = constructOptions(state);
const { locationURL } = state['features/base/connection'];
const { jwt } = state['features/base/jwt'];
const connection = new JitsiMeetJS.JitsiConnection(options.appId, jwt, options);
connection[JITSI_CONNECTION_URL_KEY] = locationURL;
dispatch(_connectionWillConnect(connection));
return new Promise((resolve, reject) => {
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_DISCONNECTED,
_onConnectionDisconnected);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
_onConnectionEstablished);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
_onConnectionFailed);
/**
* Marks the display name for the prejoin screen as required.
* This can happen if a user tries to join a room with lobby enabled.
*/
connection.addEventListener(
JitsiConnectionEvents.DISPLAY_NAME_REQUIRED,
() => dispatch(setPrejoinDisplayNameRequired())
);
/**
* Unsubscribe the connection instance from
* {@code CONNECTION_DISCONNECTED} and {@code CONNECTION_FAILED} events.
*
* @returns {void}
*/
function unsubscribe() {
connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_DISCONNECTED, _onConnectionDisconnected);
connection.removeEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _onConnectionFailed);
}
/**
* Dispatches {@code CONNECTION_DISCONNECTED} action when connection is
* disconnected.
*
* @private
* @returns {void}
*/
function _onConnectionDisconnected() {
unsubscribe();
dispatch(connectionDisconnected(connection));
resolve(connection);
}
/**
* Rejects external promise when connection fails.
*
* @param {JitsiConnectionErrors} err - Connection error.
* @param {string} [message] - Error message supplied by lib-jitsi-meet.
* @param {Object} [credentials] - The invalid credentials that were
* used to authenticate and the authentication failed.
* @param {string} [credentials.jid] - The XMPP user's ID.
* @param {string} [credentials.password] - The XMPP user's password.
* @param {Object} details - Additional information about the error.
* @private
* @returns {void}
*/
function _onConnectionFailed( // eslint-disable-line max-params
err: string,
message: string,
credentials: any,
details: Object) {
unsubscribe();
dispatch(connectionFailed(connection, {
credentials,
details,
name: err,
message
}));
reject(err);
}
/**
* Resolves external promise when connection is established.
*
* @private
* @returns {void}
*/
function _onConnectionEstablished() {
connection.removeEventListener(JitsiConnectionEvents.CONNECTION_ESTABLISHED, _onConnectionEstablished);
dispatch(connectionEstablished(connection, Date.now()));
resolve(connection);
}
connection.connect({
id,
password
});
});
};
}
/**
* Create an action for when a connection will connect.
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} which will
* connect.
* @private
* @returns {{
* type: CONNECTION_WILL_CONNECT,
* connection: JitsiConnection
* }}
*/
function _connectionWillConnect(connection: Object) {
return {
type: CONNECTION_WILL_CONNECT,
connection
};
}
/**
* Closes connection.
*
* @returns {Function}
*/
export function disconnect() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<void> => {
const state = getState();
// The conference we have already joined or are joining.
const conference_ = getCurrentConference(state);
// Promise which completes when the conference has been left and the
// connection has been disconnected.
let promise;
// Leave the conference.
if (conference_) {
// In a fashion similar to JitsiConference's CONFERENCE_LEFT event
// (and the respective Redux action) which is fired after the
// conference has been left, notify the application about the
// intention to leave the conference.
dispatch(conferenceWillLeave(conference_));
promise
= conference_.leave()
.catch((error: Error) => {
logger.warn(
'JitsiConference.leave() rejected with:',
error);
// The library lib-jitsi-meet failed to make the
// JitsiConference leave. Which may be because
// JitsiConference thinks it has already left.
// Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
} else {
promise = Promise.resolve();
}
// Disconnect the connection.
const { connecting, connection } = state['features/base/connection'];
// The connection we have already connected or are connecting.
const connection_ = connection || connecting;
if (connection_) {
promise = promise.then(() => connection_.disconnect());
} else {
logger.info('No connection found while disconnecting.');
}
return promise;
};
}

View File

@@ -1,19 +1,7 @@
import { appNavigate } from '../../app/actions.native';
import { IStore } from '../../app/types';
import { conferenceLeft, conferenceWillLeave } from '../conference/actions';
import { getCurrentConference } from '../conference/functions';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import {
CONNECTION_WILL_CONNECT
} from './actionTypes';
import {
connectionDisconnected,
connectionEstablished,
connectionFailed,
constructOptions
} from './actions.any';
import { JITSI_CONNECTION_URL_KEY } from './constants';
import logger from './logger';
import { _connectInternal } from './actions.any';
export * from './actions.any';
@@ -25,178 +13,16 @@ export * from './actions.any';
* @returns {Function}
*/
export function connect(id?: string, password?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const options = constructOptions(state);
const { locationURL } = state['features/base/connection'];
const { jwt } = state['features/base/jwt'];
const connection = new JitsiMeetJS.JitsiConnection(options.appId, jwt, options);
connection[JITSI_CONNECTION_URL_KEY] = locationURL;
dispatch(_connectionWillConnect(connection));
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_DISCONNECTED,
_onConnectionDisconnected);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
_onConnectionEstablished);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
_onConnectionFailed);
connection.connect({
id,
password
});
/**
* Dispatches {@code CONNECTION_DISCONNECTED} action when connection is
* disconnected.
*
* @private
* @returns {void}
*/
function _onConnectionDisconnected() {
unsubscribe();
dispatch(connectionDisconnected(connection));
}
/**
* Resolves external promise when connection is established.
*
* @private
* @returns {void}
*/
function _onConnectionEstablished() {
connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
_onConnectionEstablished);
dispatch(connectionEstablished(connection, Date.now()));
}
/**
* Rejects external promise when connection fails.
*
* @param {JitsiConnectionErrors} err - Connection error.
* @param {string} [msg] - Error message supplied by lib-jitsi-meet.
* @param {Object} [credentials] - The invalid credentials that were
* used to authenticate and the authentication failed.
* @param {string} [credentials.jid] - The XMPP user's ID.
* @param {string} [credentials.password] - The XMPP user's password.
* @param {Object} details - Additional information about the error.
* @private
* @returns {void}
*/
function _onConnectionFailed( // eslint-disable-line max-params
err: string,
msg: string,
credentials: any,
details: Object) {
unsubscribe();
dispatch(
connectionFailed(
connection, {
credentials,
details,
name: err,
message: msg
}
));
}
/**
* Unsubscribe the connection instance from
* {@code CONNECTION_DISCONNECTED} and {@code CONNECTION_FAILED} events.
*
* @returns {void}
*/
function unsubscribe() {
connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_DISCONNECTED,
_onConnectionDisconnected);
connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
_onConnectionFailed);
}
};
return (dispatch: IStore['dispatch']) => dispatch(_connectInternal(id, password));
}
/**
* Create an action for when a connection will connect.
* Hangup.
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} which will
* connect.
* @private
* @returns {{
* type: CONNECTION_WILL_CONNECT,
* connection: JitsiConnection
* }}
*/
function _connectionWillConnect(connection: Object) {
return {
type: CONNECTION_WILL_CONNECT,
connection
};
}
/**
* Closes connection.
*
* @param {boolean} _ - Used in web.
* @param {boolean} [_requestFeedback] - Whether to attempt showing a
* request for call feedback.
* @returns {Function}
*/
export function disconnect(_?: boolean) {
/* eslint-enable @typescript-eslint/no-unused-vars */
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<void> => {
const state = getState();
// The conference we have already joined or are joining.
const conference_ = getCurrentConference(state);
// Promise which completes when the conference has been left and the
// connection has been disconnected.
let promise;
// Leave the conference.
if (conference_) {
// In a fashion similar to JitsiConference's CONFERENCE_LEFT event
// (and the respective Redux action) which is fired after the
// conference has been left, notify the application about the
// intention to leave the conference.
dispatch(conferenceWillLeave(conference_));
promise
= conference_.leave()
.catch((error: Error) => {
logger.warn(
'JitsiConference.leave() rejected with:',
error);
// The library lib-jitsi-meet failed to make the
// JitsiConference leave. Which may be because
// JitsiConference thinks it has already left.
// Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
} else {
promise = Promise.resolve();
}
// Disconnect the connection.
const { connecting, connection } = state['features/base/connection'];
// The connection we have already connected or are connecting.
const connection_ = connection || connecting;
if (connection_) {
promise = promise.then(() => connection_.disconnect());
} else {
logger.info('No connection found while disconnecting.');
}
return promise;
};
export function hangup(_requestFeedback = false) {
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
}

View File

@@ -1,47 +1,81 @@
import { IStore } from '../../app/types';
import { configureInitialDevices } from '../devices/actions';
import { getBackendSafeRoomName } from '../util/uri';
// @ts-expect-error
import { jitsiLocalStorage } from '@jitsi/js-utils';
export {
connectionDisconnected,
connectionEstablished,
connectionFailed,
setLocationURL
} from './actions.any';
import logger from './logger';
import { IStore } from '../../app/types';
import { getCustomerDetails } from '../../jaas/actions.any';
import { getJaasJWT, isVpaasMeeting } from '../../jaas/functions';
import { showWarningNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { stopLocalVideoRecording } from '../../recording/actions.any';
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager.web';
import { setJWT } from '../jwt/actions';
import { _connectInternal } from './actions.any';
export * from './actions.any';
/**
* Opens new connection.
*
* @returns {Promise<JitsiConnection>}
* @param {string} [id] - The XMPP user's ID (e.g. {@code user@server.com}).
* @param {string} [password] - The XMPP user's password.
* @returns {Function}
*/
export function connect() {
export function connect(id?: string, password?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const room = getBackendSafeRoomName(getState()['features/base/conference'].room);
const state = getState();
const { jwt } = state['features/base/jwt'];
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
// XXX For web based version we use conference initialization logic
// from the old app (at the moment of writing).
return dispatch(configureInitialDevices()).then(
() => APP.conference.init({
roomName: room
}).catch((error: Error) => {
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
}));
if (!iAmRecorder && !iAmSipGateway && isVpaasMeeting(state)) {
return dispatch(getCustomerDetails())
.then(() => {
if (!jwt) {
return getJaasJWT(state);
}
})
.then(j => j && dispatch(setJWT(j)))
.then(() => _connectInternal(id, password));
}
// used by jibri
const usernameOverride = jitsiLocalStorage.getItem('xmpp_username_override');
const passwordOverride = jitsiLocalStorage.getItem('xmpp_password_override');
if (usernameOverride && usernameOverride.length > 0) {
id = usernameOverride; // eslint-disable-line no-param-reassign
}
if (passwordOverride && passwordOverride.length > 0) {
password = passwordOverride; // eslint-disable-line no-param-reassign
}
return dispatch(_connectInternal(id, password));
};
}
/**
* Closes connection.
*
* @param {boolean} [requestFeedback] - Whether or not to attempt showing a
* @param {boolean} [requestFeedback] - Whether to attempt showing a
* request for call feedback.
* @returns {Function}
*/
export function disconnect(requestFeedback = false) {
// XXX For web based version we use conference hanging up logic from the old
// app.
return () => APP.conference.hangup(requestFeedback);
export function hangup(requestFeedback = false) {
// XXX For web based version we use conference hanging up logic from the old app.
return async (dispatch: IStore['dispatch']) => {
if (LocalRecordingManager.isRecordingLocally()) {
dispatch(stopLocalVideoRecording());
dispatch(showWarningNotification({
titleKey: 'localRecording.stopping',
descriptionKey: 'localRecording.wait'
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
// wait 1000ms for the recording to end and start downloading
await new Promise(res => {
setTimeout(res, 1000);
});
}
return APP.conference.hangup(requestFeedback);
};
}

View File

@@ -0,0 +1,30 @@
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { CONNECTION_WILL_CONNECT } from './actionTypes';
/**
* The feature announced so we can distinguish jibri participants.
*
* @type {string}
*/
export const DISCO_JIBRI_FEATURE = 'http://jitsi.org/protocol/jibri';
MiddlewareRegistry.register(({ getState }) => next => action => {
switch (action.type) {
case CONNECTION_WILL_CONNECT: {
const { connection } = action;
const { iAmRecorder } = getState()['features/base/config'];
if (iAmRecorder) {
connection.addFeature(DISCO_JIBRI_FEATURE);
}
// @ts-ignore
APP.connection = connection;
break;
}
}
return next(action);
});

View File

@@ -243,6 +243,12 @@ export const TOOLBOX_ALWAYS_VISIBLE = 'toolbox.alwaysVisible';
*/
export const TOOLBOX_ENABLED = 'toolbox.enabled';
/**
* Flag indicating if the unsafe room warning should be enabled.
* Default: disabled (false).
*/
export const UNSAFE_ROOM_WARNING = 'unsaferoomwarning.enabled';
/**
* Flag indicating if the video mute button should be displayed.
* Default: enabled (true).

View File

@@ -142,7 +142,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
const promises = [];
// The following executes on React Native only at the time of this
// writing. The effort to port Web's createInitialLocalTracksAndConnect
// writing. The effort to port Web's createInitialLocalTracks
// is significant and that's where the function createLocalTracksF got
// born. I started with the idea a porting so that we could inherit the
// ability to getUserMedia for audio only or video only if getUserMedia

View File

@@ -1,169 +0,0 @@
import { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../app/types';
import { getLocalParticipant } from '../../base/participants/functions';
import { sendMessage, setIsPollsTabFocused } from '../actions';
import { SMALL_WIDTH_THRESHOLD } from '../constants';
import { IMessage } from '../reducer';
/**
* The type of the React {@code Component} props of {@code AbstractChat}.
*/
export interface IProps extends WithTranslation {
/**
* Whether the chat is opened in a modal or not (computed based on window width).
*/
_isModal: boolean;
/**
* True if the chat window should be rendered.
*/
_isOpen: boolean;
/**
* True if the polls feature is enabled.
*/
_isPollsEnabled: boolean;
/**
* Whether the poll tab is focused or not.
*/
_isPollsTabFocused: boolean;
/**
* All the chat messages in the conference.
*/
_messages: IMessage[];
/**
* Number of unread chat messages.
*/
_nbUnreadMessages: number;
/**
* Number of unread poll messages.
*/
_nbUnreadPolls: number;
/**
* Function to send a text message.
*
* @protected
*/
_onSendMessage: Function;
/**
* Function to toggle the chat window.
*/
_onToggleChat: Function;
/**
* Function to display the chat tab.
*
* @protected
*/
_onToggleChatTab: Function;
/**
* Function to display the polls tab.
*
* @protected
*/
_onTogglePollsTab: Function;
/**
* Whether or not to block chat access with a nickname input form.
*/
_showNamePrompt: boolean;
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
}
/**
* Implements an abstract chat panel.
*/
export default class AbstractChat<P extends IProps> extends Component<P> {
/**
* Initializes a new {@code AbstractChat} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code AbstractChat} instance with.
*/
constructor(props: P) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onSendMessage = this._onSendMessage.bind(this);
this._onToggleChatTab = this._onToggleChatTab.bind(this);
this._onTogglePollsTab = this._onTogglePollsTab.bind(this);
}
/**
* Sends a text message.
*
* @private
* @param {string} text - The text message to be sent.
* @returns {void}
* @type {Function}
*/
_onSendMessage(text: string) {
this.props.dispatch(sendMessage(text));
}
/**
* Display the Chat tab.
*
* @private
* @returns {void}
*/
_onToggleChatTab() {
this.props.dispatch(setIsPollsTabFocused(false));
}
/**
* Display the Polls tab.
*
* @private
* @returns {void}
*/
_onTogglePollsTab() {
this.props.dispatch(setIsPollsTabFocused(true));
}
}
/**
* Maps (parts of) the redux state to {@link Chat} React {@code Component}
* props.
*
* @param {Object} state - The redux store/state.
* @param {any} _ownProps - Components' own props.
* @private
* @returns {{
* _isOpen: boolean,
* _messages: Array<Object>,
* _showNamePrompt: boolean
* }}
*/
export function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, isPollsTabFocused, messages, nbUnreadMessages } = state['features/chat'];
const { nbUnreadPolls } = state['features/polls'];
const _localParticipant = getLocalParticipant(state);
const { disablePolls } = state['features/base/config'];
return {
_isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
_isOpen: isOpen,
_isPollsEnabled: !disablePolls,
_isPollsTabFocused: isPollsTabFocused,
_messages: messages,
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: nbUnreadPolls,
_showNamePrompt: !_localParticipant?.name
};
}

View File

@@ -1,88 +0,0 @@
import { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { getLocalizedDateFormatter } from '../../base/i18n/dateUtil';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../constants';
import { IMessage } from '../reducer';
/**
* Formatter string to display the message timestamp.
*/
const TIMESTAMP_FORMAT = 'H:mm';
/**
* The type of the React {@code Component} props of {@code AbstractChatMessage}.
*/
export interface IProps extends WithTranslation {
/**
* Whether current participant is currently knocking in the lobby room.
*/
knocking: boolean;
/**
* The representation of a chat message.
*/
message: IMessage;
/**
* Whether or not the avatar image of the participant which sent the message
* should be displayed.
*/
showAvatar?: boolean;
/**
* Whether or not the name of the participant which sent the message should
* be displayed.
*/
showDisplayName: boolean;
/**
* Whether or not the time at which the message was sent should be
* displayed.
*/
showTimestamp: boolean;
}
/**
* Abstract component to display a chat message.
*/
export default class AbstractChatMessage<P extends IProps> extends PureComponent<P> {
/**
* Returns the timestamp to display for the message.
*
* @returns {string}
*/
_getFormattedTimestamp() {
return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
.format(TIMESTAMP_FORMAT);
}
/**
* Generates the message text to be rendered in the component.
*
* @returns {string}
*/
_getMessageText() {
const { message } = this.props;
return message.messageType === MESSAGE_TYPE_ERROR
? this.props.t('chat.error', {
error: message.message
})
: message.message;
}
/**
* Returns the message that is displayed as a notice for private messages.
*
* @returns {string}
*/
_getPrivateNoticeMessage() {
const { message, t } = this.props;
return t('chat.privateNotice', {
recipient: message.messageType === MESSAGE_TYPE_LOCAL ? message.recipient : t('chat.you')
});
}
}

View File

@@ -1,6 +1,6 @@
import { Component } from 'react';
import { IMessage } from '../reducer';
import { IMessage } from '../types';
export interface IProps {

View File

@@ -1,16 +1,14 @@
/* eslint-disable react/no-multi-comp */
import { Route, useIsFocused } from '@react-navigation/native';
import React, { useEffect } from 'react';
import React, { Component, useEffect } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
import { closeChat } from '../../actions.native';
import AbstractChat, {
IProps as AbstractProps,
_mapStateToProps
} from '../AbstractChat';
import { closeChat, sendMessage } from '../../actions.native';
import { IChatProps as AbstractProps } from '../../types';
import ChatInputBar from './ChatInputBar';
import MessageContainer from './MessageContainer';
@@ -34,7 +32,21 @@ interface IProps extends AbstractProps {
* Implements a React native component that renders the chat window (modal) of
* the mobile client.
*/
class Chat extends AbstractChat<IProps> {
class Chat extends Component<IProps> {
/**
* Initializes a new {@code AbstractChat} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code AbstractChat} instance with.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onSendMessage = this._onSendMessage.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
@@ -57,6 +69,39 @@ class Chat extends AbstractChat<IProps> {
</JitsiScreen>
);
}
/**
* Sends a text message.
*
* @private
* @param {string} text - The text message to be sent.
* @returns {void}
* @type {Function}
*/
_onSendMessage(text: string) {
this.props.dispatch(sendMessage(text));
}
}
/**
* Maps (parts of) the redux state to {@link Chat} React {@code Component}
* props.
*
* @param {Object} state - The redux store/state.
* @param {any} _ownProps - Components' own props.
* @private
* @returns {{
* _messages: Array<Object>,
* _nbUnreadMessages: number
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { messages, nbUnreadMessages } = state['features/chat'];
return {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages
};
}
export default translate(connect(_mapStateToProps)((props: IProps) => {

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { Component } from 'react';
import { Text, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
@@ -8,8 +8,13 @@ import { translate } from '../../../base/i18n/functions';
import Linkify from '../../../base/react/components/native/Linkify';
import { isGifMessage } from '../../../gifs/functions.native';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import { replaceNonUnicodeEmojis } from '../../functions';
import AbstractChatMessage, { IProps } from '../AbstractChatMessage';
import {
getFormattedTimestamp,
getMessageText,
getPrivateNoticeMessage,
replaceNonUnicodeEmojis
} from '../../functions';
import { IChatMessageProps } from '../../types';
import GifMessage from './GifMessage';
import PrivateMessageButton from './PrivateMessageButton';
@@ -19,7 +24,7 @@ import styles from './styles';
/**
* Renders a single chat message.
*/
class ChatMessage extends AbstractChatMessage<IProps> {
class ChatMessage extends Component<IChatMessageProps> {
/**
* Implements {@code Component#render}.
*
@@ -67,7 +72,7 @@ class ChatMessage extends AbstractChatMessage<IProps> {
messageBubbleStyle.push(styles.lobbyMessageBubble);
}
const messageText = replaceNonUnicodeEmojis(this._getMessageText());
const messageText = replaceNonUnicodeEmojis(getMessageText(this.props.message));
return (
<View style = { styles.messageWrapper as ViewStyle } >
@@ -147,7 +152,7 @@ class ChatMessage extends AbstractChatMessage<IProps> {
return (
<Text style = { message.lobbyChat ? styles.lobbyMsgNotice : styles.privateNotice }>
{ this._getPrivateNoticeMessage() }
{ getPrivateNoticeMessage(this.props.message) }
</Text>
);
}
@@ -189,7 +194,7 @@ class ChatMessage extends AbstractChatMessage<IProps> {
return (
<Text style = { styles.timeText }>
{ this._getFormattedTimestamp() }
{ getFormattedTimestamp(this.props.message) }
</Text>
);
}

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { FlatList } from 'react-native';
import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants';
import { IMessage } from '../../reducer';
import { IMessage } from '../../types';
import ChatMessage from './ChatMessage';

View File

@@ -3,7 +3,7 @@ import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import { IMessage } from '../../reducer';
import { IMessage } from '../../types';
import AbstractMessageContainer, { IProps as AbstractProps } from '../AbstractMessageContainer';
import ChatMessageGroup from './ChatMessageGroup';

View File

@@ -1,16 +1,16 @@
import clsx from 'clsx';
import React from 'react';
import React, { useCallback } from 'react';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { getLocalParticipant } from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Tabs from '../../../base/ui/components/web/Tabs';
import PollsPane from '../../../polls/components/web/PollsPane';
import { toggleChat } from '../../actions.web';
import { CHAT_TABS } from '../../constants';
import AbstractChat, {
IProps,
_mapStateToProps
} from '../AbstractChat';
import { sendMessage, setIsPollsTabFocused, toggleChat } from '../../actions.web';
import { CHAT_SIZE, CHAT_TABS, SMALL_WIDTH_THRESHOLD } from '../../constants';
import { IChatProps as AbstractProps } from '../../types';
import ChatHeader from './ChatHeader';
import ChatInput from './ChatInput';
@@ -19,75 +19,167 @@ import KeyboardAvoider from './KeyboardAvoider';
import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
/**
* React Component for holding the chat feature in a side panel that slides in
* and out of view.
*/
class Chat extends AbstractChat<IProps> {
interface IProps extends AbstractProps {
/**
* Reference to the React Component for displaying chat messages. Used for
* scrolling to the end of the chat messages.
* Whether the chat is opened in a modal or not (computed based on window width).
*/
_messageContainerRef: Object;
_isModal: boolean;
/**
* Initializes a new {@code Chat} instance.
* True if the chat window should be rendered.
*/
_isOpen: boolean;
/**
* True if the polls feature is enabled.
*/
_isPollsEnabled: boolean;
/**
* Whether the poll tab is focused or not.
*/
_isPollsTabFocused: boolean;
/**
* Number of unread poll messages.
*/
_nbUnreadPolls: number;
/**
* Function to send a text message.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @protected
*/
constructor(props: IProps) {
super(props);
this._messageContainerRef = React.createRef();
// Bind event handlers so they are only bound once for every instance.
this._onChatTabKeyDown = this._onChatTabKeyDown.bind(this);
this._onEscClick = this._onEscClick.bind(this);
this._onPollsTabKeyDown = this._onPollsTabKeyDown.bind(this);
this._onToggleChat = this._onToggleChat.bind(this);
this._onChangeTab = this._onChangeTab.bind(this);
}
_onSendMessage: Function;
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
* Function to toggle the chat window.
*/
render() {
const { _isOpen, _isPollsEnabled, _showNamePrompt } = this.props;
return (
_isOpen ? <div
className = 'sideToolbarContainer'
id = 'sideToolbarContainer'
onKeyDown = { this._onEscClick } >
<ChatHeader
className = 'chat-header'
isPollsEnabled = { _isPollsEnabled }
onCancel = { this._onToggleChat } />
{ _showNamePrompt
? <DisplayNameForm isPollsEnabled = { _isPollsEnabled } />
: this._renderChat() }
</div> : null
);
}
_onToggleChat: Function;
/**
* Key press handler for the chat tab.
* Function to display the chat tab.
*
* @param {KeyboardEvent} event - The event.
* @returns {void}
* @protected
*/
_onChatTabKeyDown(event: React.KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
this._onToggleChatTab();
_onToggleChatTab: Function;
/**
* Function to display the polls tab.
*
* @protected
*/
_onTogglePollsTab: Function;
/**
* Whether or not to block chat access with a nickname input form.
*/
_showNamePrompt: boolean;
}
const useStyles = makeStyles()(theme => {
return {
container: {
backgroundColor: theme.palette.ui01,
flexShrink: 0,
overflow: 'hidden',
position: 'relative',
transition: 'width .16s ease-in-out',
width: `${CHAT_SIZE}px`,
zIndex: 300,
'@media (max-width: 580px)': {
height: '100vh',
position: 'fixed',
left: 0,
right: 0,
top: 0,
width: 'auto'
},
'*': {
userSelect: 'text',
'-webkit-user-select': 'text'
}
},
chatHeader: {
height: '60px',
position: 'relative',
width: '100%',
zIndex: 1,
display: 'flex',
justifyContent: 'space-between',
padding: `${theme.spacing(3)} ${theme.spacing(4)}`,
alignItems: 'center',
boxSizing: 'border-box',
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.heading6),
'.jitsi-icon': {
cursor: 'pointer'
}
},
chatPanel: {
display: 'flex',
flexDirection: 'column',
// extract header + tabs height
height: 'calc(100% - 110px)'
},
chatPanelNoTabs: {
// extract header height
height: 'calc(100% - 60px)'
},
pollsPanel: {
// extract header + tabs height
height: 'calc(100% - 110px)'
}
}
};
});
const Chat = ({
_isModal,
_isOpen,
_isPollsEnabled,
_isPollsTabFocused,
_messages,
_nbUnreadMessages,
_nbUnreadPolls,
_onSendMessage,
_onToggleChat,
_onToggleChatTab,
_onTogglePollsTab,
_showNamePrompt,
dispatch,
t
}: IProps) => {
const { classes, cx } = useStyles();
/**
* Sends a text message.
*
* @private
* @param {string} text - The text message to be sent.
* @returns {void}
* @type {Function}
*/
const onSendMessage = useCallback((text: string) => {
dispatch(sendMessage(text));
}, []);
/**
* Toggles the chat window.
*
* @returns {Function}
*/
const onToggleChat = useCallback(() => {
dispatch(toggleChat());
}, []);
/**
* Click handler for the chat sidenav.
@@ -95,27 +187,23 @@ class Chat extends AbstractChat<IProps> {
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event: React.KeyboardEvent) {
if (event.key === 'Escape' && this.props._isOpen) {
const onEscClick = useCallback((event: React.KeyboardEvent) => {
if (event.key === 'Escape' && _isOpen) {
event.preventDefault();
event.stopPropagation();
this._onToggleChat();
onToggleChat();
}
}
}, [ _isOpen ]);
/**
* Key press handler for the polls tab.
* Change selected tab.
*
* @param {KeyboardEvent} event - The event.
* @param {string} id - Id of the clicked tab.
* @returns {void}
*/
_onPollsTabKeyDown(event: React.KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
this._onTogglePollsTab();
}
}
const onChangeTab = useCallback((id: string) => {
dispatch(setIsPollsTabFocused(id !== CHAT_TABS.CHAT));
}, []);
/**
* Returns a React Element for showing chat messages and a form to send new
@@ -124,33 +212,31 @@ class Chat extends AbstractChat<IProps> {
* @private
* @returns {ReactElement}
*/
_renderChat() {
const { _isPollsEnabled, _isPollsTabFocused } = this.props;
function renderChat() {
return (
<>
{ _isPollsEnabled && this._renderTabs() }
{_isPollsEnabled && renderTabs()}
<div
aria-labelledby = { CHAT_TABS.CHAT }
className = { clsx(
'chat-panel',
!_isPollsEnabled && 'chat-panel-no-tabs',
className = { cx(
classes.chatPanel,
!_isPollsEnabled && classes.chatPanelNoTabs,
_isPollsTabFocused && 'hide'
) }
id = { `${CHAT_TABS.CHAT}-panel` }
role = 'tabpanel'
tabIndex = { 0 }>
<MessageContainer
messages = { this.props._messages } />
messages = { _messages } />
<MessageRecipient />
<ChatInput
onSend = { this._onSendMessage } />
onSend = { onSendMessage } />
</div>
{ _isPollsEnabled && (
{_isPollsEnabled && (
<>
<div
aria-labelledby = { CHAT_TABS.POLLS }
className = { clsx('polls-panel', !_isPollsTabFocused && 'hide') }
className = { cx(classes.pollsPanel, !_isPollsTabFocused && 'hide') }
id = { `${CHAT_TABS.POLLS}-panel` }
role = 'tabpanel'
tabIndex = { 0 }>
@@ -169,13 +255,11 @@ class Chat extends AbstractChat<IProps> {
* @private
* @returns {ReactElement}
*/
_renderTabs() {
const { _isPollsEnabled, _isPollsTabFocused, _nbUnreadMessages, _nbUnreadPolls, t } = this.props;
function renderTabs() {
return (
<Tabs
accessibilityLabel = { t(_isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
onChange = { this._onChangeTab }
onChange = { onChangeTab }
selected = { _isPollsTabFocused ? CHAT_TABS.POLLS : CHAT_TABS.CHAT }
tabs = { [ {
accessibilityLabel: t('chat.tabs.chat'),
@@ -194,24 +278,56 @@ class Chat extends AbstractChat<IProps> {
);
}
/**
* Toggles the chat window.
*
* @returns {Function}
*/
_onToggleChat() {
this.props.dispatch(toggleChat());
}
return (
_isOpen ? <div
className = { classes.container }
id = 'sideToolbarContainer'
onKeyDown = { onEscClick } >
<ChatHeader
className = { cx('chat-header', classes.chatHeader) }
isPollsEnabled = { _isPollsEnabled }
onCancel = { onToggleChat } />
{_showNamePrompt
? <DisplayNameForm isPollsEnabled = { _isPollsEnabled } />
: renderChat()}
</div> : null
);
};
/**
* Change selected tab.
*
* @param {string} id - Id of the clicked tab.
* @returns {void}
*/
_onChangeTab(id: string) {
id === CHAT_TABS.CHAT ? this._onToggleChatTab() : this._onTogglePollsTab();
}
/**
* Maps (parts of) the redux state to {@link Chat} React {@code Component}
* props.
*
* @param {Object} state - The redux store/state.
* @param {any} _ownProps - Components' own props.
* @private
* @returns {{
* _isModal: boolean,
* _isOpen: boolean,
* _isPollsEnabled: boolean,
* _isPollsTabFocused: boolean,
* _messages: Array<Object>,
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _showNamePrompt: boolean
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, isPollsTabFocused, messages, nbUnreadMessages } = state['features/chat'];
const { nbUnreadPolls } = state['features/polls'];
const _localParticipant = getLocalParticipant(state);
const { disablePolls } = state['features/base/config'];
return {
_isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
_isOpen: isOpen,
_isPollsEnabled: !disablePolls,
_isPollsTabFocused: isPollsTabFocused,
_messages: messages,
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: nbUnreadPolls,
_showNamePrompt: !_localParticipant?.name
};
}
export default translate(connect(_mapStateToProps)(Chat));

View File

@@ -1,26 +1,24 @@
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import clsx from 'clsx';
import React from 'react';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import Message from '../../../base/react/components/web/Message';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import AbstractChatMessage, { IProps as AbstractProps } from '../AbstractChatMessage';
import { getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
import { IChatMessageProps } from '../../types';
import PrivateMessageButton from './PrivateMessageButton';
interface IProps extends AbstractProps {
classes: any;
interface IProps extends IChatMessageProps {
type: string;
}
const styles = (theme: Theme) => {
const useStyles = makeStyles()((theme: Theme) => {
return {
chatMessageWrapper: {
maxWidth: '100%'
@@ -110,73 +108,35 @@ const styles = (theme: Theme) => {
marginTop: theme.spacing(1)
}
};
};
});
/**
* Renders a single chat message.
*
* @param {IProps} props - Component's props.
* @returns {JSX}
*/
class ChatMessage extends AbstractChatMessage<IProps> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { message, t, knocking, classes, type } = this.props;
return (
<div
className = { clsx(classes.chatMessageWrapper, type) }
id = { this.props.message.messageId }
tabIndex = { -1 }>
<div
className = { clsx('chatmessage', classes.chatMessage, type,
message.privateMessage && 'privatemessage',
message.lobbyChat && !knocking && 'lobbymessage') }>
<div className = { classes.replyWrapper }>
<div className = { clsx('messagecontent', classes.messageContent) }>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = { clsx('usermessage', classes.userMessage) }>
<span className = 'sr-only'>
{ this.props.message.displayName === this.props.message.recipient
? t('chat.messageAccessibleTitleMe')
: t('chat.messageAccessibleTitle',
{ user: this.props.message.displayName }) }
</span>
<Message text = { this._getMessageText() } />
</div>
{ (message.privateMessage || (message.lobbyChat && !knocking))
&& this._renderPrivateNotice() }
</div>
{ (message.privateMessage || (message.lobbyChat && !knocking))
&& message.messageType !== MESSAGE_TYPE_LOCAL
&& (
<div
className = { classes.replyButtonContainer }>
<PrivateMessageButton
isLobbyMessage = { message.lobbyChat }
participantID = { message.id } />
</div>
) }
</div>
</div>
{ this.props.showTimestamp && this._renderTimestamp() }
</div>
);
}
const ChatMessage = ({
knocking,
message,
showDisplayName,
showTimestamp,
type,
t
}: IProps) => {
const { classes, cx } = useStyles();
/**
* Renders the display name of the sender.
*
* @returns {React$Element<*>}
*/
_renderDisplayName() {
function _renderDisplayName() {
return (
<div
aria-hidden = { true }
className = { clsx('display-name', this.props.classes.displayName) }>
{ this.props.message.displayName }
className = { cx('display-name', classes.displayName) }>
{message.displayName}
</div>
);
}
@@ -186,10 +146,10 @@ class ChatMessage extends AbstractChatMessage<IProps> {
*
* @returns {React$Element<*>}
*/
_renderPrivateNotice() {
function _renderPrivateNotice() {
return (
<div className = { this.props.classes.privateMessageNotice }>
{ this._getPrivateNoticeMessage() }
<div className = { classes.privateMessageNotice }>
{getPrivateNoticeMessage(message)}
</div>
);
}
@@ -199,14 +159,54 @@ class ChatMessage extends AbstractChatMessage<IProps> {
*
* @returns {React$Element<*>}
*/
_renderTimestamp() {
function _renderTimestamp() {
return (
<div className = { clsx('timestamp', this.props.classes.timestamp) }>
{ this._getFormattedTimestamp() }
<div className = { cx('timestamp', classes.timestamp) }>
{getFormattedTimestamp(message)}
</div>
);
}
}
return (
<div
className = { cx(classes.chatMessageWrapper, type) }
id = { message.messageId }
tabIndex = { -1 }>
<div
className = { cx('chatmessage', classes.chatMessage, type,
message.privateMessage && 'privatemessage',
message.lobbyChat && !knocking && 'lobbymessage') }>
<div className = { classes.replyWrapper }>
<div className = { cx('messagecontent', classes.messageContent) }>
{showDisplayName && _renderDisplayName()}
<div className = { cx('usermessage', classes.userMessage) }>
<span className = 'sr-only'>
{message.displayName === message.recipient
? t('chat.messageAccessibleTitleMe')
: t('chat.messageAccessibleTitle',
{ user: message.displayName })}
</span>
<Message text = { getMessageText(message) } />
</div>
{(message.privateMessage || (message.lobbyChat && !knocking))
&& _renderPrivateNotice()}
</div>
{(message.privateMessage || (message.lobbyChat && !knocking))
&& message.messageType !== MESSAGE_TYPE_LOCAL
&& (
<div
className = { classes.replyButtonContainer }>
<PrivateMessageButton
isLobbyMessage = { message.lobbyChat }
participantID = { message.id } />
</div>
)}
</div>
</div>
{showTimestamp && _renderTimestamp()}
</div>
);
};
/**
* Maps part of the Redux store to the props of this component.
@@ -222,4 +222,4 @@ function _mapStateToProps(state: IReduxState) {
};
}
export default translate(connect(_mapStateToProps)(withStyles(styles)(ChatMessage)));
export default translate(connect(_mapStateToProps)(ChatMessage));

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { makeStyles } from 'tss-react/mui';
import Avatar from '../../../base/avatar/components/Avatar';
import { IMessage } from '../../reducer';
import { IMessage } from '../../types';
import ChatMessage from './ChatMessage';

View File

@@ -1,5 +1,5 @@
/**
* The size of the chat.
* The size of the chat. Equal to $sidebarWidth SCSS variable.
*/
export const CHAT_SIZE = 315;
@@ -38,3 +38,8 @@ export const CHAT_TABS = {
POLLS: 'polls-tab',
CHAT: 'chat-tab'
};
/**
* Formatter string to display the message timestamp.
*/
export const TIMESTAMP_FORMAT = 'H:mm';

View File

@@ -5,9 +5,12 @@ import aliases from 'react-emoji-render/data/aliases';
import emojiAsciiAliases from 'react-emoji-render/data/asciiAliases';
import { IReduxState } from '../app/types';
import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
import i18next from '../base/i18n/i18next';
import { escapeRegexp } from '../base/util/helpers';
import { IMessage } from './reducer';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
import { IMessage } from './types';
/**
* An ASCII emoticon regexp array to find and replace old-style ASCII
@@ -132,3 +135,40 @@ export function areSmileysDisabled(state: IReduxState) {
return disableChatSmileys;
}
/**
* Returns the timestamp to display for the message.
*
* @param {IMessage} message - The message from which to get the timestamp.
* @returns {string}
*/
export function getFormattedTimestamp(message: IMessage) {
return getLocalizedDateFormatter(new Date(message.timestamp))
.format(TIMESTAMP_FORMAT);
}
/**
* Generates the message text to be rendered in the component.
*
* @param {IMessage} message - The message from which to get the text.
* @returns {string}
*/
export function getMessageText(message: IMessage) {
return message.messageType === MESSAGE_TYPE_ERROR
? i18next.t('chat.error', {
error: message.message
})
: message.message;
}
/**
* Returns the message that is displayed as a notice for private messages.
*
* @param {IMessage} message - The message to be checked.
* @returns {string}
*/
export function getPrivateNoticeMessage(message: IMessage) {
return i18next.t('chat.privateNotice', {
recipient: message.messageType === MESSAGE_TYPE_LOCAL ? message.recipient : i18next.t('chat.you')
});
}

View File

@@ -15,6 +15,7 @@ import {
SET_LOBBY_CHAT_RECIPIENT,
SET_PRIVATE_MESSAGE_RECIPIENT
} from './actionTypes';
import { IMessage } from './types';
const DEFAULT_STATE = {
isOpen: false,
@@ -27,20 +28,6 @@ const DEFAULT_STATE = {
isLobbyChatActive: false
};
export interface IMessage {
displayName: string;
error?: Object;
id: string;
isReaction: boolean;
lobbyChat: boolean;
message: string;
messageId: string;
messageType: string;
privateMessage: boolean;
recipient: string;
timestamp: number;
}
export interface IChatState {
isLobbyChatActive: boolean;
isOpen: boolean;

View File

@@ -0,0 +1,69 @@
import { WithTranslation } from 'react-i18next';
import { IStore } from '../app/types';
export interface IMessage {
displayName: string;
error?: Object;
id: string;
isReaction: boolean;
lobbyChat: boolean;
message: string;
messageId: string;
messageType: string;
privateMessage: boolean;
recipient: string;
timestamp: number;
}
/**
* The type of the React {@code Component} props of {@code AbstractChat}.
*/
export interface IChatProps extends WithTranslation {
/**
* All the chat messages in the conference.
*/
_messages: IMessage[];
/**
* Number of unread chat messages.
*/
_nbUnreadMessages: number;
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
}
export interface IChatMessageProps extends WithTranslation {
/**
* Whether current participant is currently knocking in the lobby room.
*/
knocking: boolean;
/**
* The representation of a chat message.
*/
message: IMessage;
/**
* Whether or not the avatar image of the participant which sent the message
* should be displayed.
*/
showAvatar?: boolean;
/**
* Whether or not the name of the participant which sent the message should
* be displayed.
*/
showDisplayName: boolean;
/**
* Whether or not the time at which the message was sent should be
* displayed.
*/
showTimestamp: boolean;
}

View File

@@ -1,9 +1,12 @@
import { IStore } from '../app/types';
import { configureInitialDevices } from '../base/devices/actions.web';
import { getParticipantDisplayName } from '../base/participants/functions';
import { getBackendSafeRoomName } from '../base/util/uri';
import { showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
import logger from './logger';
/**
* Notify that we've been kicked out of the conference.
@@ -45,3 +48,24 @@ export function dismissCalendarNotification() {
type: DISMISS_CALENDAR_NOTIFICATION
};
}
/**
* Init.
*
* @returns {Promise<JitsiConnection>}
*/
export function init() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const room = getBackendSafeRoomName(getState()['features/base/conference'].room);
// XXX For web based version we use conference initialization logic
// from the old app (at the moment of writing).
return dispatch(configureInitialDevices()).then(
() => APP.conference.init({
roomName: room
}).catch((error: Error) => {
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
}));
};
}

View File

@@ -7,7 +7,7 @@ import { connect as reactReduxConnect } from 'react-redux';
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
import { IReduxState, IStore } from '../../../app/types';
import { getConferenceNameForTitle } from '../../../base/conference/functions';
import { connect, disconnect } from '../../../base/connection/actions.web';
import { hangup } from '../../../base/connection/actions.web';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { setColorAlpha } from '../../../base/util/helpers';
@@ -29,6 +29,7 @@ import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
import Toolbox from '../../../toolbox/components/web/Toolbox';
import { LAYOUT_CLASSNAMES } from '../../../video-layout/constants';
import { getCurrentLayout } from '../../../video-layout/functions.any';
import { init } from '../../actions.web';
import { maybeShowSuboptimalExperienceNotification } from '../../functions.web';
import {
AbstractConference,
@@ -187,7 +188,7 @@ class Conference extends AbstractConference<IProps, any> {
FULL_SCREEN_EVENTS.forEach(name =>
document.removeEventListener(name, this._onFullScreenChange));
APP.conference.isJoined() && this.props.dispatch(disconnect());
APP.conference.isJoined() && this.props.dispatch(hangup());
}
/**
@@ -374,7 +375,7 @@ class Conference extends AbstractConference<IProps, any> {
const { dispatch, t } = this.props;
dispatch(connect());
dispatch(init());
maybeShowSuboptimalExperienceNotification(dispatch, t);
}

View File

@@ -1,43 +1,43 @@
import React from 'react';
import { connect } from 'react-redux';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
interface IProps {
_message?: string;
}
const useStyles = makeStyles()(theme => {
return {
notice: {
position: 'absolute',
left: '50%',
zIndex: 3,
marginTop: theme.spacing(2),
transform: 'translateX(-50%)'
},
const Notice = ({ _message }: IProps) => {
if (!_message) {
message: {
backgroundColor: theme.palette.uiBackground,
color: theme.palette.text01,
padding: '3px',
borderRadius: '5px'
}
};
});
const Notice = () => {
const message = useSelector((state: IReduxState) => state['features/base/config'].noticeMessage);
const { classes } = useStyles();
if (!message) {
return null;
}
return (
<div className = 'notice'>
<span className = 'notice__message' >
{_message}
<div className = { classes.notice }>
<span className = { classes.message } >
{message}
</span>
</div>
);
};
/**
* Maps (parts of) the Redux state to the associated
* {@code Notice}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _message: string,
* }}
*/
function _mapStateToProps(state: IReduxState) {
const {
noticeMessage
} = state['features/base/config'];
return {
_message: noticeMessage
};
}
export default connect(_mapStateToProps)(Notice);
export default Notice;

View File

@@ -1,67 +0,0 @@
import { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IStore } from '../../app/types';
import { updateSettings } from '../../base/settings/actions';
/**
* The type of the React {@code Component} props of
* {@link AbstractDisplayNamePrompt}.
*/
export interface IProps extends WithTranslation {
/**
* Invoked to update the local participant's display name.
*/
dispatch: IStore['dispatch'];
/**
* Function to be invoked after a successful display name change.
*/
onPostSubmit?: Function;
}
/**
* Implements an abstract class for {@code DisplayNamePrompt}.
*/
export default class AbstractDisplayNamePrompt<S>
extends Component<IProps, S> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onSetDisplayName = this._onSetDisplayName.bind(this);
}
/**
* Dispatches an action to update the local participant's display name. A
* name must be entered for the action to dispatch.
*
* It returns a boolean to comply the Dialog behaviour:
* {@code true} - the dialog should be closed.
* {@code false} - the dialog should be left open.
*
* @param {string} displayName - The display name to save.
* @returns {boolean}
*/
_onSetDisplayName(displayName: string) {
if (!displayName?.trim()) {
return false;
}
const { dispatch, onPostSubmit } = this.props;
// Store display name in settings
dispatch(updateSettings({
displayName
}));
onPostSubmit?.();
return true;
}
}

View File

@@ -1,13 +1,29 @@
import React from 'react';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import InputDialog from '../../../base/dialog/components/native/InputDialog';
import AbstractDisplayNamePrompt from '../AbstractDisplayNamePrompt';
import { onSetDisplayName } from '../../functions';
import { IProps } from '../../types';
/**
* Implements a component to render a display name prompt.
*/
class DisplayNamePrompt extends AbstractDisplayNamePrompt<any> {
class DisplayNamePrompt extends Component<IProps> {
_onSetDisplayName: (displayName: string) => boolean;
/**
* Initializes a new {@code DisplayNamePrompt} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onSetDisplayName = onSetDisplayName(props.dispatch, props.onPostSubmit);
}
/**
* Implements React's {@link Component#render()}.
*

View File

@@ -1,10 +1,11 @@
import React from 'react';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import Dialog from '../../../base/ui/components/web/Dialog';
import Input from '../../../base/ui/components/web/Input';
import AbstractDisplayNamePrompt, { IProps } from '../AbstractDisplayNamePrompt';
import { onSetDisplayName } from '../../functions';
import { IProps } from '../../types';
/**
* The type of the React {@code Component} props of {@link DisplayNamePrompt}.
@@ -23,7 +24,9 @@ interface IState {
*
* @augments Component
*/
class DisplayNamePrompt extends AbstractDisplayNamePrompt<IState> {
class DisplayNamePrompt extends Component<IProps, IState> {
_onSetDisplayName: (displayName: string) => boolean;
/**
* Initializes a new {@code DisplayNamePrompt} instance.
*
@@ -40,6 +43,7 @@ class DisplayNamePrompt extends AbstractDisplayNamePrompt<IState> {
// Bind event handlers so they are only bound once for every instance.
this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._onSetDisplayName = onSetDisplayName(props.dispatch, props.onPostSubmit);
}
/**

View File

@@ -1,3 +1,5 @@
import { IStore } from '../app/types';
import { updateSettings } from '../base/settings/actions';
/**
* Appends a suffix to the display name.
@@ -10,3 +12,33 @@ export function appendSuffix(displayName: string, suffix = ''): string {
return `${displayName || suffix}${
displayName && suffix && displayName !== suffix ? ` (${suffix})` : ''}`;
}
/**
* Dispatches an action to update the local participant's display name. A
* name must be entered for the action to dispatch.
*
* It returns a boolean to comply the Dialog behaviour:
* {@code true} - the dialog should be closed.
* {@code false} - the dialog should be left open.
*
* @param {Function} dispatch - Redux dispatch function.
* @param {Function} onPostSubmit - Function to be invoked after a successful display name change.
* @param {string} displayName - The display name to save.
* @returns {boolean}
*/
export function onSetDisplayName(dispatch: IStore['dispatch'], onPostSubmit?: Function) {
return function(displayName: string) {
if (!displayName?.trim()) {
return false;
}
// Store display name in settings
dispatch(updateSettings({
displayName
}));
onPostSubmit?.();
return true;
};
}

View File

@@ -0,0 +1,20 @@
import { WithTranslation } from 'react-i18next';
import { IStore } from '../app/types';
/**
* The type of the React {@code Component} props of
* {@link AbstractDisplayNamePrompt}.
*/
export interface IProps extends WithTranslation {
/**
* Invoked to update the local participant's display name.
*/
dispatch: IStore['dispatch'];
/**
* Function to be invoked after a successful display name change.
*/
onPostSubmit?: Function;
}

View File

@@ -1,32 +0,0 @@
import { WithTranslation } from 'react-i18next';
import { IReduxState } from '../../app/types';
export interface IProps extends WithTranslation {
/**
* Custom e2ee labels.
*/
_e2eeLabels?: any;
/**
* True if the label needs to be rendered, false otherwise.
*/
_showLabel?: boolean;
}
/**
* Maps (parts of) the redux state to the associated props of this {@code Component}.
*
* @param {Object} state - The redux state.
* @private
* @returns {IProps}
*/
export function _mapStateToProps(state: IReduxState) {
const { e2ee = {} } = state['features/base/config'];
return {
_e2eeLabels: e2ee.labels,
_showLabel: state['features/base/participants'].numberOfParticipantsDisabledE2EE === 0
};
}

View File

@@ -1,13 +1,26 @@
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState } from '../../app/types';
import { translate } from '../../base/i18n/functions';
import { IconE2EE } from '../../base/icons/svg';
import Label from '../../base/label/components/web/Label';
import { COLORS } from '../../base/label/constants';
import Tooltip from '../../base/tooltip/components/Tooltip';
import { IProps, _mapStateToProps } from './AbstractE2EELabel';
export interface IProps extends WithTranslation {
/**
* Custom e2ee labels.
*/
_e2eeLabels?: any;
/**
* True if the label needs to be rendered, false otherwise.
*/
_showLabel?: boolean;
}
const E2EELabel = ({ _e2eeLabels, _showLabel, t }: IProps) => {
@@ -27,4 +40,20 @@ const E2EELabel = ({ _e2eeLabels, _showLabel, t }: IProps) => {
);
};
/**
* Maps (parts of) the redux state to the associated props of this {@code Component}.
*
* @param {Object} state - The redux state.
* @private
* @returns {IProps}
*/
export function _mapStateToProps(state: IReduxState) {
const { e2ee = {} } = state['features/base/config'];
return {
_e2eeLabels: e2ee.labels,
_showLabel: state['features/base/participants'].numberOfParticipantsDisabledE2EE === 0
};
}
export default translate(connect(_mapStateToProps)(E2EELabel));

View File

@@ -1,17 +1,17 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { createE2EEEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState, IStore } from '../../app/types';
import { translate } from '../../base/i18n/functions';
import Switch from '../../base/ui/components/web/Switch';
import { toggleE2EE } from '../actions';
import { MAX_MODE } from '../constants';
import { doesEveryoneSupportE2EE } from '../functions';
interface IProps extends WithTranslation {
interface IProps {
/**
* The resource for the description, computed based on the maxMode and whether the switch is toggled or not.
@@ -44,89 +44,53 @@ interface IProps extends WithTranslation {
dispatch: IStore['dispatch'];
}
interface IState {
const useStyles = makeStyles()(() => {
return {
e2eeSection: {
display: 'flex',
flexDirection: 'column'
},
/**
* True if the switch is toggled on.
*/
toggled: boolean;
}
description: {
fontSize: '13px',
margin: '15px 0'
},
controlRow: {
display: 'flex',
justifyContent: 'space-between',
marginTop: '15px',
'& label': {
fontSize: '14px',
fontWeight: 'bold'
}
}
};
});
/**
* Implements a React {@code Component} for displaying a security dialog section with a field
* for setting the E2EE key.
*
* @augments Component
* @param {IProps} props - Component's props.
* @returns {JSX}
*/
class E2EESection extends Component<IProps, IState> {
/**
* Implements React's {@link Component#getDerivedStateFromProps()}.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: IProps, state: IState) {
if (props._toggled !== state.toggled) {
const E2EESection = ({
_descriptionResource,
_enabled,
_e2eeLabels,
_everyoneSupportE2EE,
_toggled,
dispatch
}: IProps) => {
const { classes } = useStyles();
const { t } = useTranslation();
const [ toggled, setToggled ] = useState(_toggled ?? false);
return {
toggled: props._toggled
};
}
return null;
}
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this.state = {
toggled: false
};
// Bind event handlers so they are only bound once for every instance.
this._onToggle = this._onToggle.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _descriptionResource, _enabled, _e2eeLabels, _everyoneSupportE2EE, t } = this.props;
const { toggled } = this.state;
const description = _e2eeLabels?.description || t(_descriptionResource ?? '');
const label = _e2eeLabels?.label || t('dialog.e2eeLabel');
const warning = _e2eeLabels?.warning || t('dialog.e2eeWarning');
return (
<div id = 'e2ee-section'>
<p
aria-live = 'polite'
className = 'description'
id = 'e2ee-section-description'>
{ description }
{ !_everyoneSupportE2EE && <br /> }
{ !_everyoneSupportE2EE && warning }
</p>
<div className = 'control-row'>
<label htmlFor = 'e2ee-section-switch'>
{ label }
</label>
<Switch
checked = { toggled }
disabled = { !_enabled }
id = 'e2ee-section-switch'
onChange = { this._onToggle } />
</div>
</div>
);
}
useEffect(() => {
setToggled(_toggled);
}, [ _toggled ]);
/**
* Callback to be invoked when the user toggles E2EE on or off.
@@ -134,17 +98,44 @@ class E2EESection extends Component<IProps, IState> {
* @private
* @returns {void}
*/
_onToggle() {
const newValue = !this.state.toggled;
const _onToggle = useCallback(() => {
const newValue = !toggled;
this.setState({
toggled: newValue
});
setToggled(newValue);
sendAnalytics(createE2EEEvent(`enabled.${String(newValue)}`));
this.props.dispatch(toggleE2EE(newValue));
}
}
dispatch(toggleE2EE(newValue));
}, [ toggled ]);
const description = _e2eeLabels?.description || t(_descriptionResource ?? '');
const label = _e2eeLabels?.label || t('dialog.e2eeLabel');
const warning = _e2eeLabels?.warning || t('dialog.e2eeWarning');
return (
<div
className = { classes.e2eeSection }
id = 'e2ee-section'>
<p
aria-live = 'polite'
className = { classes.description }
id = 'e2ee-section-description'>
{description}
{!_everyoneSupportE2EE && <br />}
{!_everyoneSupportE2EE && warning}
</p>
<div className = { classes.controlRow }>
<label htmlFor = 'e2ee-section-switch'>
{label}
</label>
<Switch
checked = { toggled }
disabled = { !_enabled }
id = 'e2ee-section-switch'
onChange = { _onToggle } />
</div>
</div>
);
};
/**
* Maps (parts of) the Redux state to the associated props for this component.
@@ -180,4 +171,4 @@ function mapStateToProps(state: IReduxState) {
};
}
export default translate(connect(mapStateToProps)(E2EESection));
export default connect(mapStateToProps)(E2EESection);

View File

@@ -1,15 +1,13 @@
import { Theme } from '@mui/material';
import { ClassNameMap, withStyles } from '@mui/styles';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { createFeedbackOpenEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState, IStore } from '../../app/types';
import { IReduxState } from '../../app/types';
import { IJitsiConference } from '../../base/conference/reducer';
import { isMobileBrowser } from '../../base/environment/utils';
import { translate } from '../../base/i18n/functions';
import Icon from '../../base/icons/components/Icon';
import { IconFavorite, IconFavoriteSolid } from '../../base/icons/svg';
import { withPixelLineHeight } from '../../base/styles/functions.web';
@@ -17,7 +15,7 @@ import Dialog from '../../base/ui/components/web/Dialog';
import Input from '../../base/ui/components/web/Input';
import { cancelFeedback, submitFeedback } from '../actions.web';
const styles = (theme: Theme) => {
const useStyles = makeStyles()(theme => {
return {
dialog: {
marginBottom: theme.spacing(1)
@@ -68,7 +66,7 @@ const styles = (theme: Theme) => {
}
}
};
};
});
/**
* The scores to display for selecting. The score is the index in the array and
@@ -84,31 +82,10 @@ const SCORES = [
const ICON_SIZE = 32;
type Scrollable = {
scroll: Function;
};
/**
* The type of the React {@code Component} props of {@link FeedbackDialog}.
*/
interface IProps extends WithTranslation {
/**
* The cached feedback message, if any, that was set when closing a previous
* instance of {@code FeedbackDialog}.
*/
_message: string;
/**
* The cached feedback score, if any, that was set when closing a previous
* instance of {@code FeedbackDialog}.
*/
_score: number;
/**
* An object containing the CSS classes.
*/
classes: ClassNameMap<string>;
interface IProps {
/**
* The JitsiConference that is being rated. The conference is passed in
@@ -117,236 +94,74 @@ interface IProps extends WithTranslation {
*/
conference: IJitsiConference;
/**
* Invoked to signal feedback submission or canceling.
*/
dispatch: IStore['dispatch'];
/**
* Callback invoked when {@code FeedbackDialog} is unmounted.
*/
onClose: Function;
}
/**
* The type of the React {@code Component} state of {@link FeedbackDialog}.
*/
interface IState {
/**
* The currently entered feedback message.
*/
message: string;
/**
* The score selection index which is currently being hovered. The value -1
* is used as a sentinel value to match store behavior of using -1 for no
* score having been selected.
*/
mousedOverScore: number;
/**
* The currently selected score selection index. The score will not be 0
* indexed so subtract one to map with SCORES.
*/
score: number;
}
/**
* A React {@code Component} for displaying a dialog to rate the current
* conference quality, write a message describing the experience, and submit
* the feedback.
*
* @augments Component
* @param {IProps} props - Component's props.
* @returns {JSX}
*/
class FeedbackDialog extends Component<IProps, IState> {
const FeedbackDialog = ({ conference, onClose }: IProps) => {
const { classes } = useStyles();
const dispatch = useDispatch();
const { t } = useTranslation();
const _message = useSelector((state: IReduxState) => state['features/feedback'].message);
const _score = useSelector((state: IReduxState) => state['features/feedback'].score);
/**
* The currently entered feedback message.
*/
const [ message, setMessage ] = useState(_message);
/**
* The score selection index which is currently being hovered. The
* value -1 is used as a sentinel value to match store behavior of
* using -1 for no score having been selected.
*/
const [ mousedOverScore, setMousedOverScore ] = useState(-1);
/**
* The currently selected score selection index. The score will not
* be 0 indexed so subtract one to map with SCORES.
*/
const [ score, setScore ] = useState(_score > -1 ? _score - 1 : _score);
/**
* An array of objects with click handlers for each of the scores listed in
* the constant SCORES. This pattern is used for binding event handlers only
* once for each score selection icon.
*/
_scoreClickConfigurations: Array<{
_onClick: (e: React.MouseEvent) => void;
_onKeyDown: (e: React.KeyboardEvent) => void;
_onMouseOver: (e: React.MouseEvent) => void;
}>;
_onScrollTop: (node: Scrollable | null) => void;
/**
* Initializes a new {@code FeedbackDialog} instance.
*
* @param {Object} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
const { _message, _score } = this.props;
this.state = {
/**
* The currently entered feedback message.
*
* @type {string}
*/
message: _message,
/**
* The score selection index which is currently being hovered. The
* value -1 is used as a sentinel value to match store behavior of
* using -1 for no score having been selected.
*
* @type {number}
*/
mousedOverScore: -1,
/**
* The currently selected score selection index. The score will not
* be 0 indexed so subtract one to map with SCORES.
*
* @type {number}
*/
score: _score > -1 ? _score - 1 : _score
const scoreClickConfigurations = useRef(SCORES.map((textKey, index) => {
return {
_onClick: () => onScoreSelect(index),
_onKeyDown: (e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
onScoreSelect(index);
}
},
_onMouseOver: () => onScoreMouseOver(index)
};
}));
this._scoreClickConfigurations = SCORES.map((textKey, index) => {
return {
_onClick: () => this._onScoreSelect(index),
_onKeyDown: (e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
this._onScoreSelect(index);
}
},
_onMouseOver: () => this._onScoreMouseOver(index)
};
});
// Bind event handlers so they are only bound once for every instance.
this._onCancel = this._onCancel.bind(this);
this._onMessageChange = this._onMessageChange.bind(this);
this._onScoreContainerMouseLeave
= this._onScoreContainerMouseLeave.bind(this);
this._onSubmit = this._onSubmit.bind(this);
// On some mobile browsers opening Feedback dialog scrolls down the whole content because of the keyboard.
// By scrolling to the top we prevent hiding the feedback stars so the user knows those exist.
this._onScrollTop = (node: Scrollable | null) => {
node?.scroll?.(0, 0);
};
}
/**
* Emits an analytics event to notify feedback has been opened.
*
* @inheritdoc
*/
componentDidMount() {
useEffect(() => {
sendAnalytics(createFeedbackOpenEvent());
if (typeof APP !== 'undefined') {
APP.API.notifyFeedbackPromptDisplayed();
}
}
/**
* Invokes the onClose callback, if defined, to notify of the close event.
*
* @inheritdoc
*/
componentWillUnmount() {
if (this.props.onClose) {
this.props.onClose();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { message, mousedOverScore, score } = this.state;
const scoreToDisplayAsSelected
= mousedOverScore > -1 ? mousedOverScore : score;
const { classes, t } = this.props;
const scoreIcons = this._scoreClickConfigurations.map(
(config, index) => {
const isFilled = index <= scoreToDisplayAsSelected;
const activeClass = isFilled ? 'active' : '';
const className
= `${classes.starBtn} ${activeClass}`;
return (
<span
aria-label = { t(SCORES[index]) }
className = { className }
key = { index }
onClick = { config._onClick }
onKeyDown = { config._onKeyDown }
role = 'button'
tabIndex = { 0 }
{ ...(isMobileBrowser() ? {} : {
onMouseOver: config._onMouseOver
}) }>
{ isFilled
? <Icon
size = { ICON_SIZE }
src = { IconFavoriteSolid } />
: <Icon
size = { ICON_SIZE }
src = { IconFavorite } /> }
</span>
);
});
return (
<Dialog
ok = {{
translationKey: 'dialog.Submit'
}}
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
size = 'large'
titleKey = 'feedback.rateExperience'>
<div className = { classes.dialog }>
<div className = { classes.rating }>
<div
className = { classes.stars }
onMouseLeave = { this._onScoreContainerMouseLeave }>
{ scoreIcons }
</div>
<div
className = { classes.ratingLabel } >
<p className = 'sr-only'>
{ t('feedback.accessibilityLabel.yourChoice', {
rating: t(SCORES[scoreToDisplayAsSelected])
}) }
</p>
<p
aria-hidden = { true }
id = 'starLabel'>
{ t(SCORES[scoreToDisplayAsSelected]) }
</p>
</div>
</div>
<div className = { classes.details }>
<Input
id = 'feedbackTextArea'
label = { t('feedback.detailsLabel') }
onChange = { this._onMessageChange }
textarea = { true }
value = { message } />
</div>
</div>
</Dialog>
);
}
return () => {
onClose?.();
};
}, []);
/**
* Dispatches an action notifying feedback was not submitted. The submitted
@@ -356,14 +171,13 @@ class FeedbackDialog extends Component<IProps, IState> {
* @private
* @returns {boolean} Returns true to close the dialog.
*/
_onCancel() {
const { message, score } = this.state;
const onCancel = useCallback(() => {
const scoreToSubmit = score > -1 ? score + 1 : score;
this.props.dispatch(cancelFeedback(scoreToSubmit, message));
dispatch(cancelFeedback(scoreToSubmit, message));
return true;
}
}, [ score, message ]);
/**
* Updates the known entered feedback message.
@@ -373,19 +187,19 @@ class FeedbackDialog extends Component<IProps, IState> {
* @private
* @returns {void}
*/
_onMessageChange(newValue: string) {
this.setState({ message: newValue });
}
const onMessageChange = useCallback((newValue: string) => {
setMessage(newValue);
}, []);
/**
* Updates the currently selected score.
*
* @param {number} score - The index of the selected score in SCORES.
* @param {number} newScore - The index of the selected score in SCORES.
* @private
* @returns {void}
*/
_onScoreSelect(score: number) {
this.setState({ score });
function onScoreSelect(newScore: number) {
setScore(newScore);
}
/**
@@ -395,20 +209,20 @@ class FeedbackDialog extends Component<IProps, IState> {
* @private
* @returns {void}
*/
_onScoreContainerMouseLeave() {
this.setState({ mousedOverScore: -1 });
}
const onScoreContainerMouseLeave = useCallback(() => {
setMousedOverScore(-1);
}, []);
/**
* Updates the known state of the score icon currently behind hovered over.
*
* @param {number} mousedOverScore - The index of the SCORES value currently
* @param {number} newMousedOverScore - The index of the SCORES value currently
* being moused over.
* @private
* @returns {void}
*/
_onScoreMouseOver(mousedOverScore: number) {
this.setState({ mousedOverScore });
function onScoreMouseOver(newMousedOverScore: number) {
setMousedOverScore(newMousedOverScore);
}
/**
@@ -418,46 +232,89 @@ class FeedbackDialog extends Component<IProps, IState> {
* @private
* @returns {boolean} Returns true to close the dialog.
*/
_onSubmit() {
const { conference, dispatch } = this.props;
const { message, score } = this.state;
const _onSubmit = useCallback(() => {
const scoreToSubmit = score > -1 ? score + 1 : score;
dispatch(submitFeedback(scoreToSubmit, message, conference));
return true;
}
}
}, [ score, message, conference ]);
/**
* Maps (parts of) the Redux state to the associated {@code FeedbackDialog}'s
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { message, score } = state['features/feedback'];
const scoreToDisplayAsSelected
= mousedOverScore > -1 ? mousedOverScore : score;
return {
/**
* The cached feedback message, if any, that was set when closing a
* previous instance of {@code FeedbackDialog}.
*
* @type {string}
*/
_message: message,
const scoreIcons = scoreClickConfigurations.current.map(
(config, index) => {
const isFilled = index <= scoreToDisplayAsSelected;
const activeClass = isFilled ? 'active' : '';
const className
= `${classes.starBtn} ${activeClass}`;
/**
* The currently selected score selection index.
*
* @type {number}
*/
_score: score
};
}
return (
<span
aria-label = { t(SCORES[index]) }
className = { className }
key = { index }
onClick = { config._onClick }
onKeyDown = { config._onKeyDown }
role = 'button'
tabIndex = { 0 }
{ ...(isMobileBrowser() ? {} : {
onMouseOver: config._onMouseOver
}) }>
{isFilled
? <Icon
size = { ICON_SIZE }
src = { IconFavoriteSolid } />
: <Icon
size = { ICON_SIZE }
src = { IconFavorite } />}
</span>
);
});
export default withStyles(styles)(translate(connect(_mapStateToProps)(FeedbackDialog)));
return (
<Dialog
ok = {{
translationKey: 'dialog.Submit'
}}
onCancel = { onCancel }
onSubmit = { _onSubmit }
size = 'large'
titleKey = 'feedback.rateExperience'>
<div className = { classes.dialog }>
<div className = { classes.rating }>
<div
className = { classes.stars }
onMouseLeave = { onScoreContainerMouseLeave }>
{scoreIcons}
</div>
<div
className = { classes.ratingLabel } >
<p className = 'sr-only'>
{t('feedback.accessibilityLabel.yourChoice', {
rating: t(SCORES[scoreToDisplayAsSelected])
})}
</p>
<p
aria-hidden = { true }
id = 'starLabel'>
{t(SCORES[scoreToDisplayAsSelected])}
</p>
</div>
</div>
<div className = { classes.details }>
<Input
id = 'feedbackTextArea'
label = { t('feedback.detailsLabel') }
onChange = { onMessageChange }
textarea = { true }
value = { message } />
</div>
</div>
</Dialog>
);
};
export default FeedbackDialog;

View File

@@ -1,61 +0,0 @@
import React, { Component } from 'react';
import { IReduxState } from '../../app/types';
import { getParticipantById, hasRaisedHand } from '../../base/participants/functions';
export interface IProps {
/**
* True if the hand is raised for this participant.
*/
_raisedHand?: boolean;
/**
* The participant id who we want to render the raised hand indicator
* for.
*/
participantId: string;
}
/**
* Implements an abstract class for the RaisedHandIndicator component.
*/
export default abstract class AbstractRaisedHandIndicator<P extends IProps>
extends Component<P> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
if (!this.props._raisedHand) {
return null;
}
return this._renderIndicator();
}
/**
* Renders the platform specific indicator element.
*
* @returns {React$Element<*>}
*/
abstract _renderIndicator(): React.ReactElement;
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {IProps} ownProps - The own props of the component.
* @returns {Object}
*/
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const participant = getParticipantById(state, ownProps.participantId);
return {
_raisedHand: hasRaisedHand(participant)
};
}

View File

@@ -1,23 +1,49 @@
import React from 'react';
import React, { Component } from 'react';
import { View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IconRaiseHand } from '../../../base/icons/svg';
import { getParticipantById, hasRaisedHand } from '../../../base/participants/functions';
import BaseIndicator from '../../../base/react/components/native/BaseIndicator';
import AbstractRaisedHandIndicator, {
IProps,
_mapStateToProps
} from '../AbstractRaisedHandIndicator';
import styles from './styles';
export interface IProps {
/**
* True if the hand is raised for this participant.
*/
_raisedHand?: boolean;
/**
* The participant id who we want to render the raised hand indicator
* for.
*/
participantId: string;
}
/**
* Thumbnail badge showing that the participant would like to speak.
*
* @augments Component
*/
class RaisedHandIndicator extends AbstractRaisedHandIndicator<IProps> {
class RaisedHandIndicator extends Component<IProps> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
if (!this.props._raisedHand) {
return null;
}
return this._renderIndicator();
}
/**
* Renders the platform specific indicator element.
*
@@ -34,4 +60,19 @@ class RaisedHandIndicator extends AbstractRaisedHandIndicator<IProps> {
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {IProps} ownProps - The own props of the component.
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const participant = getParticipantById(state, ownProps.participantId);
return {
_raisedHand: hasRaisedHand(participant)
};
}
export default connect(_mapStateToProps)(RaisedHandIndicator);

View File

@@ -10,7 +10,7 @@ import { getFeatureFlag } from '../../base/flags/functions';
import { getLocalParticipant } from '../../base/participants/functions';
import { getFieldValue } from '../../base/react/functions';
import { updateSettings } from '../../base/settings/actions';
import { IMessage } from '../../chat/reducer';
import { IMessage } from '../../chat/types';
import { isDeviceStatusVisible } from '../../prejoin/functions';
import { cancelKnocking, joinWithPassword, onSendMessage, setPasswordJoinFailed, startKnocking } from '../actions';

View File

@@ -1,14 +1,14 @@
/**
* The type of redux action to set the AppState API change event listener.
* The type of redux action used for app state subscription.
*
* {
* type: _SET_APP_STATE_LISTENER,
* listener: Function
* type: _SET_APP_STATE_SUBSCRIPTION,
* subscription: NativeEventSubscription
* }
*
* @protected
*/
export const _SET_APP_STATE_LISTENER = '_SET_APP_STATE_LISTENER';
export const _SET_APP_STATE_SUBSCRIPTION = '_SET_APP_STATE_SUBSCRIPTION';
/**
* The type of redux action which signals that the app state has changed (in

View File

@@ -1,19 +1,21 @@
import { APP_STATE_CHANGED, _SET_APP_STATE_LISTENER } from './actionTypes';
import { NativeEventSubscription } from 'react-native';
import { APP_STATE_CHANGED, _SET_APP_STATE_SUBSCRIPTION } from './actionTypes';
/**
* Sets the listener to be used with React Native's AppState API.
* Sets subscription for app state.
*
* @param {Function} listener - Function to be set as the change event listener.
* @protected
* @param {Function} subscription - Subscription for the native event.
* @private
* @returns {{
* type: _SET_APP_STATE_LISTENER,
* listener: Function
* type: _SET_APP_STATE_SUBSCRIPTION,
* subscription: NativeEventSubscription
* }}
*/
export function _setAppStateListener(listener?: Function) {
export function _setAppStateSubscription(subscription?: NativeEventSubscription) {
return {
type: _SET_APP_STATE_LISTENER,
listener
type: _SET_APP_STATE_SUBSCRIPTION,
subscription
};
}

View File

@@ -5,11 +5,7 @@ import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import { _SET_APP_STATE_LISTENER } from './actionTypes';
import {
_setAppStateListener as _setAppStateListenerA,
appStateChanged
} from './actions';
import { _setAppStateSubscription, appStateChanged } from './actions';
/**
* Middleware that captures App lifetime actions and subscribes to application
@@ -23,18 +19,16 @@ import {
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case _SET_APP_STATE_LISTENER:
return _setAppStateListenerF(store, next, action);
case APP_WILL_MOUNT: {
const { dispatch } = store;
dispatch(_setAppStateListenerA(_onAppStateChange.bind(undefined, dispatch)));
_setAppStateListener(store, next, action, _onAppStateChange.bind(undefined, dispatch));
break;
}
case APP_WILL_UNMOUNT:
store.dispatch(_setAppStateListenerA(undefined));
_setAppStateListener(store, next, action, undefined);
break;
}
@@ -65,19 +59,16 @@ function _onAppStateChange(dispatch: IStore['dispatch'], appState: string) {
* specified action to the specified store.
* @param {Action} action - The redux action {@code _SET_IMMERSIVE_LISTENER}
* which is being dispatched in the specified store.
* @param {any} listener - Listener for app state status.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setAppStateListenerF({ getState }: IStore, next: Function, action: AnyAction) {
// Remove the old AppState listener and add the new one.
const { appStateListener: oldListener } = getState()['features/background'];
function _setAppStateListener({ dispatch, getState }: IStore, next: Function, action: AnyAction, listener: any) {
const { subscription } = getState()['features/background'];
const result = next(action);
const { appStateListener: newListener } = getState()['features/background'];
if (oldListener !== newListener) {
oldListener && AppState.removeEventListener('change', oldListener);
newListener && AppState.addEventListener('change', newListener);
}
subscription?.remove();
listener && dispatch(_setAppStateSubscription(AppState.addEventListener('change', listener)));
return result;
}

View File

@@ -1,15 +1,12 @@
import { AppStateStatus } from 'react-native';
import { NativeEventSubscription } from 'react-native';
import ReducerRegistry from '../../base/redux/ReducerRegistry';
import {
APP_STATE_CHANGED,
_SET_APP_STATE_LISTENER
} from './actionTypes';
import { APP_STATE_CHANGED, _SET_APP_STATE_SUBSCRIPTION } from './actionTypes';
export interface IBackgroundState {
appState: string;
appStateListener?: (state: AppStateStatus) => void;
subscription?: NativeEventSubscription;
}
/**
@@ -21,10 +18,11 @@ const DEFAULT_STATE = {
ReducerRegistry.register<IBackgroundState>('features/background', (state = DEFAULT_STATE, action): IBackgroundState => {
switch (action.type) {
case _SET_APP_STATE_LISTENER:
case _SET_APP_STATE_SUBSCRIPTION:
return {
...state,
appStateListener: action.listener
subscription: action.subscription
};
case APP_STATE_CHANGED:

View File

@@ -8,6 +8,7 @@ import { IReduxState, IStore } from '../../../app/types';
import DialInSummary from '../../../invite/components/dial-in-summary/native/DialInSummary';
import Prejoin from '../../../prejoin/components/native/Prejoin';
import UnsafeRoomWarning from '../../../prejoin/components/native/UnsafeRoomWarning';
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
// eslint-disable-next-line
// @ts-ignore
import WelcomePage from '../../../welcome/components/WelcomePage';
@@ -39,6 +40,11 @@ interface IProps {
*/
dispatch: IStore['dispatch'];
/**
* Is unsafe room warning available?
*/
isUnsafeRoomWarningAvailable: boolean;
/**
* Is welcome page available?
*/
@@ -46,7 +52,7 @@ interface IProps {
}
const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: IProps) => {
const RootNavigationContainer = ({ dispatch, isUnsafeRoomWarningAvailable, isWelcomePageAvailable }: IProps) => {
const initialRouteName = isWelcomePageAvailable
? screen.welcome.main : screen.connecting;
const onReady = useCallback(() => {
@@ -92,10 +98,13 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: IProps) =
component = { Prejoin }
name = { screen.preJoin }
options = { preJoinScreenOptions } />
<RootStack.Screen
component = { UnsafeRoomWarning }
name = { screen.unsafeRoomWarning }
options = { unsafeMeetingScreenOptions } />
{
isUnsafeRoomWarningAvailable
&& <RootStack.Screen
component = { UnsafeRoomWarning }
name = { screen.unsafeRoomWarning }
options = { unsafeMeetingScreenOptions } />
}
<RootStack.Screen
component = { ConferenceNavigationContainer }
name = { screen.conference.root }
@@ -113,6 +122,7 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: IProps) =
*/
function mapStateToProps(state: IReduxState) {
return {
isUnsafeRoomWarningAvailable: isUnsafeRoomWarningEnabled(state),
isWelcomePageAvailable: isWelcomePageEnabled(state)
};
}

View File

@@ -23,13 +23,13 @@ const externalAPIEnabled = isExternalAPIAvailable();
switch (type) {
case READY_TO_CLOSE:
rnSdkHandlers.onReadyToClose && rnSdkHandlers.onReadyToClose();
rnSdkHandlers?.onReadyToClose && rnSdkHandlers?.onReadyToClose();
break;
case CONFERENCE_JOINED:
rnSdkHandlers.onConferenceJoined && rnSdkHandlers.onConferenceJoined();
rnSdkHandlers?.onConferenceJoined && rnSdkHandlers?.onConferenceJoined();
break;
case CONFERENCE_WILL_JOIN:
rnSdkHandlers.onConferenceWillJoin && rnSdkHandlers.onConferenceWillJoin();
rnSdkHandlers?.onConferenceWillJoin && rnSdkHandlers?.onConferenceWillJoin();
break;
case CONFERENCE_LEFT:
// Props are torn down at this point, perhaps need to leave this one out
@@ -38,7 +38,7 @@ const externalAPIEnabled = isExternalAPIAvailable();
const { participant } = action;
const participantInfo = participantToParticipantInfo(participant);
rnSdkHandlers.onParticipantJoined && rnSdkHandlers.onParticipantJoined(participantInfo);
rnSdkHandlers?.onParticipantJoined && rnSdkHandlers?.onParticipantJoined(participantInfo);
break;
}
}

View File

@@ -0,0 +1,14 @@
import {
SET_PREJOIN_DISPLAY_NAME_REQUIRED
} from './actionTypes';
/**
* Action used to set the stance of the display name.
*
* @returns {Object}
*/
export function setPrejoinDisplayNameRequired() {
return {
type: SET_PREJOIN_DISPLAY_NAME_REQUIRED
};
}

View File

@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
import { IStore } from '../app/types';
import { updateConfig } from '../base/config/actions';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { connect } from '../base/connection/actions';
import { browser } from '../base/lib-jitsi-meet';
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
import { MEDIA_TYPE } from '../base/media/constants';
@@ -31,7 +32,6 @@ import {
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_PRECALL_TEST_RESULTS,
SET_PREJOIN_DEVICE_ERRORS,
SET_PREJOIN_DISPLAY_NAME_REQUIRED,
SET_PREJOIN_PAGE_VISIBILITY,
SET_SKIP_PREJOIN_RELOAD
} from './actionTypes';
@@ -66,6 +66,8 @@ const STATUS_REQ_FREQUENCY = 2000;
*/
const STATUS_REQ_CAP = 45;
export * from './actions.any';
/**
* Polls for status change after dial out.
* Changes dialog message based on response, closes the dialog if there is an error,
@@ -226,33 +228,37 @@ 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}`);
dispatch(connect()).then(async () => {
// TODO keep this here till we move tracks and conference management from
// conference.js to react.
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);
const jitsiTracks = localTracks.map((t: any) => t.jitsiTrack);
APP.conference.prejoinStart(jitsiTracks);
APP.conference.startConference(jitsiTracks).catch(logger.error);
});
};
}
@@ -465,17 +471,6 @@ export function setDialOutCountry(value: Object) {
};
}
/**
* Action used to set the stance of the display name.
*
* @returns {Object}
*/
export function setPrejoinDisplayNameRequired() {
return {
type: SET_PREJOIN_DISPLAY_NAME_REQUIRED
};
}
/**
* Action used to set the dial out number.
*

View File

@@ -1,9 +1,12 @@
import { IReduxState } from '../app/types';
import { getRoomName } from '../base/conference/functions';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions.any';
import { UNSAFE_ROOM_WARNING } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import { isAudioMuted, isVideoMutedByUser } from '../base/media/functions';
import { getLobbyConfig } from '../lobby/functions';
/**
* Selector for the visibility of the 'join by phone' button.
*
@@ -169,3 +172,14 @@ export function shouldAutoKnock(state: IReduxState): boolean {
|| autoKnock || (iAmRecorder && iAmSipGateway))
&& !state['features/lobby'].knocking);
}
/**
* Returns true if the unsafe room warning flag is enabled.
*
* @param {IReduxState} stateful - The state of the app.
* @returns {boolean}
*/
export function isUnsafeRoomWarningEnabled(stateful: IReduxState): boolean {
return Boolean(navigator.product === 'ReactNative'
&& getFeatureFlag(stateful, UNSAFE_ROOM_WARNING, true));
}

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