Compare commits

..

25 Commits

Author SHA1 Message Date
Hristo Terezov
350443ad34 fix(large-video): Attempt to fix jumping.
When the toolbox is hidden and due to a ReactFocusLock instance the
focus is returned to the toolbox the whole page scrolls to the toolbox
which is positioned outside of the viewport in the bottom.
Then when the animation for displaying the toolbox is started the
scenario looks like the large video is jumping.
Now we don't return the focus from ReactFocusLock to elements which are
not part of the viewport.
2023-05-03 17:31:54 -05:00
Jaya Allamsetty
4c37ef7a2c ref(conference) Simplify track creation. (#13209)
* ref(conference) Simplify track creation.
If gUM fails, we do not have to retry gUM with mic only and camera only constraints. gUM has come a long way and this is not needed anymore.

* ref(conference) Filter tracks that are added to conference.

* squash: Address review comments

* fix(prejoin): Display the exact gUM error in prejoin.

* squash: Address review comments
2023-05-03 18:16:48 -04:00
damencho
3eedc2a49d fix: Restore old config for promoted visitors.
In case a visitor is promoted to main room and want to join an empty breakout room we want to send conference iq to jicofo.
2023-05-03 11:04:40 -05:00
FIKRAT HUSEYNKHANOV
aaeb1a90e5 feat: add toggleWhiteboard to Jitsi API (#13292)
* add toggleWhiteboard to Jitsi API

* eslint recommendations applied

* Prevent to send whiteboard status change notifications for mobile

* Fix code style errors (eslint)

* Requested changes (by mihhu) have been made.

---------

Co-authored-by: Fikret Huseynkhanov <fikret.huseynkhanov@simbrella.com>
2023-05-03 09:52:44 -05:00
Saúl Ibarra Corretgé
ed89f9af20 feat(android) add support for Hearing Aid devices 2023-05-03 09:09:10 +02:00
Saúl Ibarra Corretgé
863ad0b0e6 fix(ios) bump WebRTC version to fix crash
Fix a crash in Metal rendering: 0c9cc42025
2023-05-02 23:12:56 +02:00
Дамян Минков
e2d701a8cc feat: Audio output settings in visitor mode. (#13315)
* feat: Audio output settings in visitor mode.

* squash: Fix lint errors.
2023-05-02 16:06:19 -05:00
damencho
2710273069 feat: Sets meeting id using the connect method. 2023-05-02 12:40:08 -05:00
damencho
07af18e284 fix: Handles disconnect coming from jicofo to destroy visitor rooms.
Drops destroy room for no main participants from vnode. In case of breakout rooms we can end up with nobody in the main room for some time, till they are back from the breakout rooms.
2023-05-02 12:40:08 -05:00
damencho
519e37f567 fix: Drops console warn for logger. 2023-05-02 12:40:08 -05:00
damencho
7cf61eb776 fix: Disables p2p for visitors as it doesn't make sense.
The usecase is breakout rooms when main room is empty, but then one of the participants come back.
2023-05-02 12:40:08 -05:00
Jaya Allamsetty
f81446909c chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1626.0.0+a41aa571...v1629.0.0+1714bf07
2023-05-02 10:01:10 -04:00
damencho
b4115593c0 fix: Fix xmldom version to be used. 2023-05-02 08:29:10 -05:00
Hristo Terezov
f8bd8b616e feat(reactions): New button for web. 2023-05-02 08:20:35 -05:00
Hristo Terezov
be55ccd6f4 fix(main-Toolbox): Display the correct buttons.
If some of the buttons from the main toolbar are disabled we were
displaying buttons from the overflow menu in their place.
2023-05-02 08:20:35 -05:00
Horatiu Muresan
e7db18bd80 fix(dial-in) Place PIN on a new line (#13309) 2023-05-02 16:02:51 +03:00
Robert Pintilii
dff41e0fcb fix(chat) Fix horizontal scroll (#13308) 2023-05-02 15:53:30 +03:00
damencho
43be4324af fix: Fix room locking without visitors. 2023-05-02 06:25:37 -05:00
Saúl Ibarra Corretgé
c33baf4c96 fix(ios) avoid rejecting builds in progress in TestFlight 2023-05-02 10:21:30 +02:00
Saúl Ibarra Corretgé
f95a356025 feat(android) bump minimum API level to 24
Some of our dependencies (most notably WebRTC) have dropped it and we
can no longer claim to support API level 23).
2023-05-02 10:19:19 +02:00
Robert Pintilii
1ba7765898 ref(TS) Convert some native components to TS (#13281)
Remove some @ts-ignores
2023-05-02 11:09:38 +03:00
Robert Pintilii
0346fca434 fix(checkbox) Fix misalign when label has multiple lines (#13304) 2023-05-02 10:23:04 +03:00
Robert Pintilii
d267b2499d fix(chat) Fix name overflows chat bubble (#13303)
Revert color change of scroll corner
2023-05-02 10:22:49 +03:00
Дамян Минков
e3e5f1fbfa feat(visitors): Handles locked rooms for visitors. (#13296)
* feat(visitors): Handles locked rooms for visitors.

* squash: Handle locked room password on promotion.

* squash: quotes.

* squash: Renames main_domain to local_domain.

* squash: Renames fmuc_main_domain to main_domain.

Adds required config to point to the main virtual host of the main prosody. There are cases when the first visitor tries to join and there are not main participants as they are in the queue waiting for the vnode connect message and we cannot get dynamically the main domain.

* squash: Fix check for main_domain config.
2023-05-01 17:16:16 -05:00
robertpin
4697192b43 fix(keyboard-a11y) Remove space from click trigger 2023-05-01 16:50:23 +02:00
125 changed files with 1700 additions and 1239 deletions

View File

@@ -45,6 +45,12 @@ class AudioDeviceHandlerGeneric implements
*/
private AudioModeModule module;
/**
* Constant defining a Hearing Aid. Only available on API level >= 28.
* The value of: AudioDeviceInfo.TYPE_HEARING_AID
*/
private static final int TYPE_HEARING_AID = 23;
/**
* Constant defining a USB headset. Only available on API level >= 26.
* The value of: AudioDeviceInfo.TYPE_USB_HEADSET
@@ -85,6 +91,7 @@ class AudioDeviceHandlerGeneric implements
break;
case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
case AudioDeviceInfo.TYPE_WIRED_HEADSET:
case TYPE_HEARING_AID:
case TYPE_USB_HEADSET:
devices.add(AudioModeModule.DEVICE_HEADPHONES);
break;

View File

@@ -559,7 +559,7 @@ export default {
);
}
let tryCreateLocalTracks;
let tryCreateLocalTracks = Promise.resolve([]);
// On Electron there is no permission prompt for granting permissions. That's why we don't need to
// spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
@@ -600,76 +600,65 @@ export default {
return [];
});
} else if (!requestedAudio && !requestedVideo) {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
} else if (requestedAudio || requestedVideo) {
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
timeout,
firePermissionPromptIsShownEvent: true
})
.catch(err => {
if (requestedAudio && requestedVideo) {
// Try audio only...
errors.audioAndVideoError = err;
if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
// In this case we expect that the permission prompt is still visible. There is no point of
// executing GUM with different source. Also at the time of writing the following
// inconsistency have been noticed in some browsers - if the permissions prompt is visible
// and another GUM is executed the prompt does not change its content but if the user
// clicks allow the user action isassociated with the latest GUM call.
errors.audioOnlyError = err;
errors.videoOnlyError = err;
return [];
}
return createLocalTracksF(audioOptions);
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
return [];
} else if (requestedVideo && !requestedAudio) {
errors.videoOnlyError = err;
return [];
}
logger.error('Should never happen');
})
.catch(err => {
// Log this just in case...
if (!requestedAudio) {
logger.error('The impossible just happened', err);
}
errors.audioOnlyError = err;
// Try video only...
return requestedVideo
? createLocalTracksF({
devices: [ MEDIA_TYPE.VIDEO ],
firePermissionPromptIsShownEvent: true
})
: [];
})
.catch(err => {
// Log this just in case...
if (!requestedVideo) {
logger.error('The impossible just happened', err);
}
errors.videoOnlyError = err;
.catch(async error => {
if (error.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
errors.audioAndVideoError = error;
return [];
}
// Retry with separate gUM calls.
const gUMPromises = [];
const tracks = [];
if (requestedAudio) {
gUMPromises.push(createLocalTracksF(audioOptions));
}
if (requestedVideo) {
gUMPromises.push(createLocalTracksF({
devices: [ MEDIA_TYPE.VIDEO ],
timeout,
firePermissionPromptIsShownEvent: true
}));
}
const results = await Promise.allSettled(gUMPromises);
let errorMsg;
results.forEach((result, idx) => {
if (result.status === 'fulfilled') {
tracks.push(result.value[0]);
} else {
errorMsg = result.reason;
const isAudio = idx === 0;
logger.error(`${isAudio ? 'Audio' : 'Video'} track creation failed with error ${errorMsg}`);
if (isAudio) {
errors.audioOnlyError = errorMsg;
} else {
errors.videoOnlyError = errorMsg;
}
}
});
if (errors.audioOnlyError && errors.videoOnlyError) {
errors.audioAndVideoError = errorMsg;
}
return tracks;
});
}
// Hide the permissions prompt/overlay as soon as the tracks are
// created. Don't wait for the connection to be made, since in some
// cases, when auth is required, for instance, that won't happen until
// the user inputs their credentials, but the dialog would be
// overshadowed by the overlay.
// Hide the permissions prompt/overlay as soon as the tracks are created. Don't wait for the connection to
// be established, as in some cases like when auth is required, connection won't be established until the user
// inputs their credentials, but the dialog would be overshadowed by the overlay.
tryCreateLocalTracks.then(tracks => {
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
@@ -810,43 +799,51 @@ export default {
const initialOptions = {
startAudioOnly: config.startAudioOnly,
startScreenSharing: config.startScreenSharing,
startWithAudioMuted: getStartWithAudioMuted(state)
|| isUserInteractionRequiredForUnmute(state),
startWithVideoMuted: getStartWithVideoMuted(state)
|| isUserInteractionRequiredForUnmute(state)
startWithAudioMuted: getStartWithAudioMuted(state) || isUserInteractionRequiredForUnmute(state),
startWithVideoMuted: getStartWithVideoMuted(state) || isUserInteractionRequiredForUnmute(state)
};
this.roomName = roomName;
try {
// Initialize the device list first. This way, when creating tracks
// based on preferred devices, loose label matching can be done in
// cases where the exact ID match is no longer available, such as
// when the camera device has switched USB ports.
// when in startSilent mode we want to start with audio muted
// Initialize the device list first. This way, when creating tracks based on preferred devices, loose label
// matching can be done in cases where the exact ID match is no longer available, such as -
// 1. When the camera device has switched USB ports.
// 2. When in startSilent mode we want to start with audio muted
await this._initDeviceList();
} catch (error) {
logger.warn('initial device list initialization failed', error);
}
const handleStartAudioMuted = (options, tracks) => {
if (options.startWithAudioMuted) {
// Filter out the local tracks based on various config options, i.e., when user joins muted or is muted by
// focus. However, audio track will always be created even though it is not added to the conference since we
// want audio related features (noisy mic, talk while muted, etc.) to work even if the mic is muted.
const handleInitialTracks = (options, tracks) => {
let localTracks = tracks;
// No local tracks are added when user joins as a visitor.
if (iAmVisitor(state)) {
return [];
}
if (options.startWithAudioMuted || room?.isStartAudioMuted()) {
// Always add the track on Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted, i.e., if there is no local media capture.
if (browser.isWebKitBased()) {
this.muteAudio(true, true);
} else {
return tracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
}
}
if (room?.isStartVideoMuted()) {
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.VIDEO);
}
return tracks;
return localTracks;
};
if (isPrejoinPageVisible(state)) {
_connectionPromise = connect(roomName).then(c => {
// we want to initialize it early, in case of errors to be able
// to gather logs
// We want to initialize it early, in case of errors to be able to gather logs.
APP.connection = c;
return c;
@@ -859,48 +856,28 @@ export default {
APP.store.dispatch(makePrecallTest(this._getConferenceOptions()));
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
const tracks = await tryCreateLocalTracks;
const localTracks = await tryCreateLocalTracks;
// Initialize device list a second time to ensure device labels
// get populated in case of an initial gUM acceptance; otherwise
// they may remain as empty strings.
// Initialize device list a second time to ensure device labels get populated in case of an initial gUM
// acceptance; otherwise they may remain as empty strings.
this._initDeviceList(true);
if (isPrejoinPageVisible(state)) {
return APP.store.dispatch(initPrejoin(tracks, errors));
return APP.store.dispatch(initPrejoin(localTracks, errors));
}
logger.debug('Prejoin screen no longer displayed at the time when tracks were created');
this._displayErrorsForCreateInitialLocalTracks(errors);
let localTracks = handleStartAudioMuted(initialOptions, tracks);
// In case where gUM is slow and resolves after the startAudio/VideoMuted coming from jicofo, we can be
// join unmuted even though jicofo had instruct us to mute, so let's respect that before passing the tracks
if (!browser.isWebKitBased()) {
if (room?.isStartAudioMuted()) {
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
}
}
if (room?.isStartVideoMuted()) {
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.VIDEO);
}
// Do not add the tracks if the user has joined the call as a visitor.
if (iAmVisitor(state)) {
return Promise.resolve();
}
return this._setLocalAudioVideoStreams(localTracks);
return this._setLocalAudioVideoStreams(handleInitialTracks(initialOptions, localTracks));
}
const [ tracks, con ] = await this.createInitialLocalTracksAndConnect(roomName, initialOptions);
this._initDeviceList(true);
return this.startConference(con, handleStartAudioMuted(initialOptions, tracks));
return this.startConference(con, handleInitialTracks(initialOptions, tracks));
},
/**
@@ -1073,7 +1050,7 @@ export default {
*/
muteVideo(mute, showUI = true) {
if (this.videoSwitchInProgress) {
console.warn('muteVideo - unable to perform operations while video switch is in progress');
logger.warn('muteVideo - unable to perform operations while video switch is in progress');
return;
}

View File

@@ -74,10 +74,6 @@
a:active {
color: black;
}
&::-webkit-scrollbar-corner {
background: #3a3a3a;
}
}

View File

@@ -134,7 +134,7 @@ PODS:
- AppAuth/Core (~> 1.6)
- GTMSessionFetcher/Core (< 3.0, >= 1.5)
- GTMSessionFetcher/Core (2.3.0)
- JitsiWebRTC (111.0.1)
- JitsiWebRTC (111.0.2)
- libwebp (1.2.4):
- libwebp/demux (= 1.2.4)
- libwebp/mux (= 1.2.4)
@@ -725,7 +725,7 @@ SPEC CHECKSUMS:
GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749
GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd
GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2
JitsiWebRTC: 9619c1f71cc16eeca76df68aa2d213c6d63274a8
JitsiWebRTC: 80f62908fcf2a1160e0d14b584323fb6e6be630b
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
ObjectiveDropboxOfficial: fe206ce8c0bc49976c249d472db7fdbc53ebbd53

View File

@@ -98,7 +98,6 @@ platform :ios do
demo_account_required: false,
distribute_external: true,
groups: ENV["JITSI_BETA_TESTING_GROUPS"],
reject_build_waiting_for_review: true,
uses_non_exempt_encryption: false
)

View File

@@ -1150,6 +1150,7 @@
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Raise your hand",
"reactions": "Reactions",
"reactionsMenu": "Reactions menu",
"recording": "Toggle recording",
"remoteMute": "Mute participant",
@@ -1247,6 +1248,7 @@
"reactionLike": "Send thumbs up reaction",
"reactionSilence": "Send silence reaction",
"reactionSurprised": "Send surprised reaction",
"reactions": "Reactions",
"security": "Security options",
"selectBackground": "Select background",
"shareRoom": "Invite someone",

View File

@@ -113,6 +113,8 @@ import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/function
import { setTileView, toggleTileView } from '../../react/features/video-layout/actions.any';
import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality/actions';
import { toggleWhiteboard } from '../../react/features/whiteboard/actions.any';
import { WhiteboardStatus } from '../../react/features/whiteboard/types';
import { getJitsiMeetTransport } from '../transport';
import {
@@ -833,6 +835,9 @@ function initCommands() {
} else {
logger.error(' End Conference not supported');
}
},
'toggle-whiteboard': () => {
APP.store.dispatch(toggleWhiteboard());
}
};
transport.on('event', ({ data, name }) => {
@@ -2014,6 +2019,20 @@ class API {
});
}
/**
* Notify external application (if API is enabled) if whiteboard state is
* changed.
*
* @param {WhiteboardStatus} status - The new whiteboard status.
* @returns {void}
*/
notifyWhiteboardStatusChanged(status: WhiteboardStatus) {
this._sendEvent({
name: 'whiteboard-status-changed',
status
});
}
/**
* Disposes the allocated resources.
*

View File

@@ -90,7 +90,8 @@ const commands = {
toggleSubtitles: 'toggle-subtitles',
toggleTileView: 'toggle-tile-view',
toggleVirtualBackgroundDialog: 'toggle-virtual-background',
toggleVideo: 'toggle-video'
toggleVideo: 'toggle-video',
toggleWhiteboard: 'toggle-whiteboard'
};
/**
@@ -154,7 +155,8 @@ const events = {
'subject-change': 'subjectChange',
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged',
'toolbar-button-clicked': 'toolbarButtonClicked'
'toolbar-button-clicked': 'toolbarButtonClicked',
'whiteboard-status-changed': 'whiteboardStatusChanged'
};
/**

153
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/v1626.0.0+a41aa571/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1629.0.0+1714bf07/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -71,7 +71,7 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"react-emoji-render": "1.2.4",
"react-focus-lock": "2.5.1",
"react-focus-lock": "2.9.4",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.68.6",
@@ -10508,9 +10508,9 @@
}
},
"node_modules/focus-lock": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.9.2.tgz",
"integrity": "sha512-YtHxjX7a0IC0ZACL5wsX8QdncXofWpGPNoVMuI/nZUrPGp6LmNI6+D5j0pPj+v8Kw5EpweA+T5yImK0rnWf7oQ==",
"version": "0.11.6",
"resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.6.tgz",
"integrity": "sha512-KSuV3ur4gf2KqMNoZx3nXNVhqCkn42GuTYCX4tXPEwf0MjpFQmNMiN6m7dXaUXgIoivL6/65agoUMg4RLS0Vbg==",
"dependencies": {
"tslib": "^2.0.3"
},
@@ -12739,8 +12739,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1626.0.0+a41aa571/lib-jitsi-meet.tgz",
"integrity": "sha512-0LK5EMCvtsAEQkjkrWzyniTl8BKtshAZxI3e9hGnA/RVDuULLXre7GEWd2zCP3tCfdxclkhqt7skQpfNhCTdqg==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1629.0.0+1714bf07/lib-jitsi-meet.tgz",
"integrity": "sha512-2gpi4oZwYFlNDOhsS3VC8eC0pR3vL6ekXBY+mFKe+gpRAh1d4S+st1r5kr7HGnkA/KF75+Ae5wDVq11knKJPRg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -15415,14 +15415,14 @@
}
},
"node_modules/react-clientside-effect": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.5.tgz",
"integrity": "sha512-2bL8qFW1TGBHozGGbVeyvnggRpMjibeZM2536AKNENLECutp2yfs44IL8Hmpn8qjFQ2K7A9PnYf3vc7aQq/cPA==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz",
"integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==",
"dependencies": {
"@babel/runtime": "^7.12.13"
},
"peerDependencies": {
"react": "^15.3.0 || ^16.0.0 || ^17.0.0"
"react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-devtools-core": {
@@ -15484,19 +15484,25 @@
}
},
"node_modules/react-focus-lock": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.1.tgz",
"integrity": "sha512-gOToRZKVEymGEjFaTRUKgJsdYQrNosoiK7yZnXnnd8bYew4vMzk3Rxb0Q4nyrGwsFuUmgQiSAulQirA0J+v4hA==",
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.4.tgz",
"integrity": "sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg==",
"dependencies": {
"@babel/runtime": "^7.0.0",
"focus-lock": "^0.9.1",
"focus-lock": "^0.11.6",
"prop-types": "^15.6.2",
"react-clientside-effect": "^1.2.2",
"use-callback-ref": "^1.2.1",
"use-sidecar": "^1.0.1"
"react-clientside-effect": "^1.2.6",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0"
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-freeze": {
@@ -17627,15 +17633,6 @@
"ws": "^8.5.0"
}
},
"node_modules/strophe.js/node_modules/@xmldom/xmldom": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.2.tgz",
"integrity": "sha512-+R0juSseERyoPvnBQ/cZih6bpF7IpCXlWbHRoCRzYzqpz6gWHOgf8o4MOEf6KBVuOyqU+gCNLkCWVIJAro8XyQ==",
"optional": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/strophe.js/node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
@@ -18572,15 +18569,18 @@
}
},
"node_modules/use-callback-ref": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz",
"integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=8.5.0"
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0",
"react": "^16.8.0 || ^17.0.0"
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -18639,25 +18639,26 @@
"integrity": "sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ=="
},
"node_modules/use-sidecar": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz",
"integrity": "sha512-k9jnrjYNwN6xYLj1iaGhonDghfvmeTmYjAiGvOr7clwKfPjMXJf4/HOr7oT5tJwYafgp2tG2l3eZEOfoELiMcA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
"dependencies": {
"detect-node-es": "^1.1.0",
"tslib": "^1.9.3"
"tslib": "^2.0.0"
},
"engines": {
"node": ">=8.5.0"
"node": ">=10"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0"
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/use-subscription": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz",
@@ -27450,9 +27451,9 @@
"integrity": "sha512-1gIBiWJNR0tKUNv8gZuk7l9rVX06OuLzY9AoGio7y/JT4V1IZErEMEq2TJS+PFcw/y0RshZ1J/27VfK1UQzYVg=="
},
"focus-lock": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.9.2.tgz",
"integrity": "sha512-YtHxjX7a0IC0ZACL5wsX8QdncXofWpGPNoVMuI/nZUrPGp6LmNI6+D5j0pPj+v8Kw5EpweA+T5yImK0rnWf7oQ==",
"version": "0.11.6",
"resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.6.tgz",
"integrity": "sha512-KSuV3ur4gf2KqMNoZx3nXNVhqCkn42GuTYCX4tXPEwf0MjpFQmNMiN6m7dXaUXgIoivL6/65agoUMg4RLS0Vbg==",
"requires": {
"tslib": "^2.0.3"
}
@@ -29117,8 +29118,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1626.0.0+a41aa571/lib-jitsi-meet.tgz",
"integrity": "sha512-0LK5EMCvtsAEQkjkrWzyniTl8BKtshAZxI3e9hGnA/RVDuULLXre7GEWd2zCP3tCfdxclkhqt7skQpfNhCTdqg==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1629.0.0+1714bf07/lib-jitsi-meet.tgz",
"integrity": "sha512-2gpi4oZwYFlNDOhsS3VC8eC0pR3vL6ekXBY+mFKe+gpRAh1d4S+st1r5kr7HGnkA/KF75+Ae5wDVq11knKJPRg==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
@@ -31170,9 +31171,9 @@
}
},
"react-clientside-effect": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.5.tgz",
"integrity": "sha512-2bL8qFW1TGBHozGGbVeyvnggRpMjibeZM2536AKNENLECutp2yfs44IL8Hmpn8qjFQ2K7A9PnYf3vc7aQq/cPA==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz",
"integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==",
"requires": {
"@babel/runtime": "^7.12.13"
}
@@ -31216,16 +31217,16 @@
}
},
"react-focus-lock": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.1.tgz",
"integrity": "sha512-gOToRZKVEymGEjFaTRUKgJsdYQrNosoiK7yZnXnnd8bYew4vMzk3Rxb0Q4nyrGwsFuUmgQiSAulQirA0J+v4hA==",
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.4.tgz",
"integrity": "sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg==",
"requires": {
"@babel/runtime": "^7.0.0",
"focus-lock": "^0.9.1",
"focus-lock": "^0.11.6",
"prop-types": "^15.6.2",
"react-clientside-effect": "^1.2.2",
"use-callback-ref": "^1.2.1",
"use-sidecar": "^1.0.1"
"react-clientside-effect": "^1.2.6",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
}
},
"react-freeze": {
@@ -32849,18 +32850,12 @@
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.5.0.tgz",
"integrity": "sha512-H5tE/tZxPR5xP3jhXyQwsjnMSwQMf7vrn9r1OkufTApyGHYe8WjzhsfxtL3AFhVu7vFjXPPZBrmUOTm1ccYgOA==",
"requires": {
"@xmldom/xmldom": "0.8.2",
"@xmldom/xmldom": "0.8.7",
"abab": "^2.0.3",
"karma-rollup-preprocessor": "^7.0.8",
"ws": "^8.5.0"
},
"dependencies": {
"@xmldom/xmldom": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.2.tgz",
"integrity": "sha512-+R0juSseERyoPvnBQ/cZih6bpF7IpCXlWbHRoCRzYzqpz6gWHOgf8o4MOEf6KBVuOyqU+gCNLkCWVIJAro8XyQ==",
"optional": true
},
"ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
@@ -33536,9 +33531,12 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
},
"use-callback-ref": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz",
"integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==",
"requires": {
"tslib": "^2.0.0"
}
},
"use-composed-ref": {
"version": "1.2.1",
@@ -33569,19 +33567,12 @@
"integrity": "sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ=="
},
"use-sidecar": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz",
"integrity": "sha512-k9jnrjYNwN6xYLj1iaGhonDghfvmeTmYjAiGvOr7clwKfPjMXJf4/HOr7oT5tJwYafgp2tG2l3eZEOfoELiMcA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
"requires": {
"detect-node-es": "^1.1.0",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
"tslib": "^2.0.0"
}
},
"use-subscription": {

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/v1626.0.0+a41aa571/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1629.0.0+1714bf07/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -76,7 +76,7 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"react-emoji-render": "1.2.4",
"react-focus-lock": "2.5.1",
"react-focus-lock": "2.9.4",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.68.6",
@@ -179,7 +179,7 @@
"webpack-dev-server": "4.7.3"
},
"overrides": {
"strophe.js@1.6.0": {
"strophe.js@1.5.0": {
"@xmldom/xmldom": "0.8.7"
}
},

View File

@@ -300,7 +300,8 @@ export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
if (!vnode) {
// this is redirecting back to main, lets restore config
// no point of updating disableFocus, we can skip the initial iq to jicofo
// not updating disableFocus, as if the room capacity is full the promotion to the main room will fail
// and the visitor will be redirected back to a vnode from jicofo
if (config.oldConfig && username) {
return {
hosts: {
@@ -310,6 +311,7 @@ export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
focusUserJid: focusJid,
disableLocalStats: false,
bosh: config.oldConfig.bosh && appendURLParam(config.oldConfig.bosh, 'customusername', username),
p2p: config.oldConfig.p2p,
websocket: config.oldConfig.websocket
&& appendURLParam(config.oldConfig.websocket, 'customusername', username),
oldConfig: undefined // clears it up
@@ -326,6 +328,7 @@ export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
},
focusUserJid: config.focusUserJid,
bosh: config.bosh,
p2p: config.p2p,
websocket: config.websocket
};
@@ -341,6 +344,10 @@ export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
disableFocus: true, // This flag disables sending the initial conference request
disableLocalStats: true,
bosh: config.bosh && appendURLParam(config.bosh, 'vnode', vnode),
p2p: {
...config.p2p,
enabled: false
},
websocket: config.websocket && appendURLParam(config.websocket, 'vnode', vnode)
};
}

View File

@@ -289,6 +289,12 @@ function _conferenceJoined({ dispatch, getState }: IStore, next: Function, actio
dispatch(conferenceWillLeave(conference));
};
if (!iAmVisitor(getState())) {
// if a visitor is promoted back to main room and want to join an empty breakout room
// we need to send iq to jicofo, so it can join/create the breakout room
dispatch(overwriteConfig({ disableFocus: false }));
}
// @ts-ignore
window.addEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);

View File

@@ -18,6 +18,7 @@ type ToolbarButtons = 'camera' |
'participants-pane' |
'profile' |
'raisehand' |
'reactions' |
'recording' |
'security' |
'select-background' |

View File

@@ -64,7 +64,7 @@ export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-bac
/**
* The toolbar buttons to show when in visitors mode.
*/
export const VISITORS_MODE_BUTTONS = [ 'chat', 'hangup', 'raisehand', 'tileview' ];
export const VISITORS_MODE_BUTTONS = [ 'chat', 'hangup', 'raisehand', 'settings', 'tileview' ];
/**
* The set of feature flags.

View File

@@ -84,6 +84,7 @@ export interface IConfigState extends IConfig {
domain: string;
muc: string;
};
p2p?: object;
websocket?: string;
};
}

View File

@@ -36,7 +36,7 @@ type Props = {
/**
* Function to render a bottom sheet footer element, if necessary.
*/
renderFooter?: Function;
renderFooter?: () => React.ReactNode;
/**
* Function to render a bottom sheet header element, if necessary.

View File

@@ -18,7 +18,7 @@ interface IProps extends AbstractProps, WithTranslation {
/**
* The dialog descriptionKey.
*/
descriptionKey: string;
descriptionKey?: string;
/**
* An optional initial value to initiate the field with.

View File

@@ -6,6 +6,7 @@ import { IReduxState } from '../../../app/types';
import DialogPortal from '../../../toolbox/components/web/DialogPortal';
import Drawer from '../../../toolbox/components/web/Drawer';
import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
import { isElementInTheViewport } from '../../ui/functions.web';
import { getContextMenuStyle } from '../functions.web';
/**
@@ -258,7 +259,16 @@ class Popover extends Component<IProps, IState> {
'aria-labelledby': headingId,
'aria-label': !headingId && headingLabel ? headingLabel : undefined
}}
returnFocus = { true }>
returnFocus = {
// If we return the focus to an element outside the viewport the page will scroll to
// this element which in our case is undesirable and the element is outside of the
// viewport on purpose (to be hidden). For example if we return the focus to the toolbox
// when it is hidden the whole page will move up in order to show the toolbox. This is
// usually followed up with displaying the toolbox (because now it is on focus) but
// because of the animation the whole scenario looks like jumping large video.
isElementInTheViewport
}>
{this._renderContent()}
</ReactFocusLock>
</DialogPortal>
@@ -304,7 +314,8 @@ class Popover extends Component<IProps, IState> {
&& !this.props.overflowDrawer
&& this._contextMenuRef
&& this._contextMenuRef.contains
&& !this._contextMenuRef.contains(event.target as Node)) {
&& !this._contextMenuRef.contains(event.target as Node)
&& !this._containerRef?.current?.contains(event.target as Node)) {
this._onHideDialog();
}
}

View File

@@ -43,7 +43,7 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
* @returns {void}
*/
_onKeyPress(event?: React.KeyboardEvent) {
if (event?.key === 'Enter' || event?.key === ' ') {
if (event?.key === 'Enter') {
event.preventDefault();
this.props.onClick();
}

View File

@@ -33,17 +33,17 @@ interface IProps {
/**
* Icon of the button.
*/
icon: Function;
icon?: Function;
/**
* Flag used for disabling the small icon.
*/
iconDisabled: boolean;
iconDisabled?: boolean;
/**
* The ID of the icon button.
*/
iconId: string;
iconId?: string;
/**
* Popover close callback.
@@ -65,6 +65,11 @@ interface IProps {
*/
styles?: Object;
/**
* Whether the trigger for open/ close should be click or hover.
*/
trigger?: 'hover' | 'click';
/**
* Whether or not the popover is visible.
*/
@@ -77,7 +82,7 @@ interface IProps {
* @param {Object} props - Component's props.
* @returns {ReactElement}
*/
export default function ToolboxButtonWithIconPopup(props: IProps) {
export default function ToolboxButtonWithPopup(props: IProps) {
const {
ariaControls,
ariaExpanded,
@@ -91,9 +96,29 @@ export default function ToolboxButtonWithIconPopup(props: IProps) {
onPopoverOpen,
popoverContent,
styles,
trigger,
visible
} = props;
if (!icon) {
return (
<div
className = 'settings-button-container'
style = { styles }>
<Popover
content = { popoverContent }
headingLabel = { ariaLabel }
onPopoverClose = { onPopoverClose }
onPopoverOpen = { onPopoverOpen }
position = 'top'
trigger = { trigger }
visible = { visible }>
{children}
</Popover>
</div>
);
}
const iconProps: any = {};
if (iconDisabled) {

View File

@@ -1,8 +1,10 @@
import { IStore } from '../../app/types';
import { IStateful } from '../app/types';
import { isMobileBrowser } from '../environment/utils';
import JitsiMeetJS from '../lib-jitsi-meet';
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { setAudioMuted } from '../media/actions';
import { MEDIA_TYPE } from '../media/constants';
import { getStartWithAudioMuted } from '../media/functions';
import { toState } from '../redux/functions';
import {
getUserSelectedCameraDeviceId,
@@ -94,10 +96,10 @@ export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore)
}
/**
* Returns an object containing a promise which resolves with the created tracks &
* the errors resulting from that process.
* Returns an object containing a promise which resolves with the created tracks and the errors resulting from that
* process.
*
* @returns {Promise<JitsiLocalTrack>}
* @returns {Promise<JitsiLocalTrack[]>}
*
* @todo Refactor to not use APP.
*/
@@ -106,7 +108,13 @@ export function createPrejoinTracks() {
const initialDevices = [ 'audio' ];
const requestedAudio = true;
let requestedVideo = false;
const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
const { startAudioOnly, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
const startWithAudioMuted = getStartWithAudioMuted(APP.store.getState());
// On Electron there is no permission prompt for granting permissions. That's why we don't need to
// spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
// probably never resolve.
const timeout = browser.isElectron() ? 15000 : 60000;
// Always get a handle on the audio input device so that we have statistics even if the user joins the
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
@@ -121,62 +129,66 @@ export function createPrejoinTracks() {
requestedVideo = true;
}
let tryCreateLocalTracks;
let tryCreateLocalTracks: any = Promise.resolve([]);
if (!requestedAudio && !requestedVideo) {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
if (requestedAudio || requestedVideo) {
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
firePermissionPromptIsShownEvent: true
firePermissionPromptIsShownEvent: true,
timeout
}, APP.store)
.catch((err: Error) => {
if (requestedAudio && requestedVideo) {
.catch(async (err: Error) => {
if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
errors.audioAndVideoError = err;
// Try audio only...
errors.audioAndVideoError = err;
return [];
}
return (
createLocalTracksF({
devices: [ 'audio' ],
firePermissionPromptIsShownEvent: true
}));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
// Retry with separate gUM calls.
const gUMPromises: any = [];
const tracks: any = [];
return [];
} else if (requestedVideo && !requestedAudio) {
errors.videoOnlyError = err;
if (requestedAudio) {
gUMPromises.push(createLocalTracksF({
devices: [ MEDIA_TYPE.AUDIO ],
firePermissionPromptIsShownEvent: true,
timeout
}));
}
return [];
if (requestedVideo) {
gUMPromises.push(createLocalTracksF({
devices: [ MEDIA_TYPE.VIDEO ],
firePermissionPromptIsShownEvent: true,
timeout
}));
}
const results = await Promise.allSettled(gUMPromises);
let errorMsg;
results.forEach((result, idx) => {
if (result.status === 'fulfilled') {
tracks.push(result.value[0]);
} else {
errorMsg = result.reason;
const isAudio = idx === 0;
logger.error(`${isAudio ? 'Audio' : 'Video'} track creation failed with error ${errorMsg}`);
if (isAudio) {
errors.audioOnlyError = errorMsg;
} else {
errors.videoOnlyError = errorMsg;
}
logger.error('Should never happen');
})
.catch((err: Error) => {
// Log this just in case...
if (!requestedAudio) {
logger.error('The impossible just happened', err);
}
errors.audioOnlyError = err;
}
});
// Try video only...
return requestedVideo
? createLocalTracksF({
devices: [ 'video' ],
firePermissionPromptIsShownEvent: true
})
: [];
})
.catch((err: Error) => {
// Log this just in case...
if (!requestedVideo) {
logger.error('The impossible just happened', err);
}
errors.videoOnlyError = err;
if (errors.audioOnlyError && errors.videoOnlyError) {
errors.audioAndVideoError = errorMsg;
}
return [];
});
return tracks;
});
}
return {

View File

@@ -5,6 +5,7 @@ import { keyframes } from 'tss-react';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { isElementInTheViewport } from '../../functions.web';
import { DialogTransitionContext } from './DialogTransition';
@@ -184,7 +185,16 @@ const BaseDialog = ({
onClick = { onBackdropClick } />
<FocusLock
className = { classes.focusLock }
returnFocus = { true }>
returnFocus = {
// If we return the focus to an element outside the viewport the page will scroll to
// this element which in our case is undesirable and the element is outside of the
// viewport on purpose (to be hidden). For example if we return the focus to the toolbox
// when it is hidden the whole page will move up in order to show the toolbox. This is
// usually followed up with displaying the toolbox (because now it is on focus) but
// because of the animation the whole scenario looks like jumping large video.
isElementInTheViewport
}>
<div
aria-describedby = { description }
aria-labelledby = { title ?? t(titleKey ?? '') }

View File

@@ -70,7 +70,7 @@ const useStyles = makeStyles()(theme => {
'& input[type="checkbox"]': {
appearance: 'none',
backgroundColor: 'transparent',
margin: 0,
margin: '3px',
font: 'inherit',
color: theme.palette.icon03,
width: '18px',

View File

@@ -57,3 +57,28 @@ export const findAncestorByClass = (target: HTMLElement | null, cssClass: string
return findAncestorByClass(target.parentElement, cssClass);
};
/**
* Checks if the passed element is visible in the viewport.
*
* @param {Element} element - The element.
* @returns {boolean}
*/
export function isElementInTheViewport(element?: Element): boolean {
if (!element) {
return false;
}
if (!document.body.contains(element)) {
return false;
}
const { innerHeight, innerWidth } = window;
const { bottom, left, right, top } = element.getBoundingClientRect();
if (bottom <= innerHeight && top >= 0 && left >= 0 && right <= innerWidth) {
return true;
}
return false;
}

View File

@@ -1,6 +1,5 @@
/* eslint-disable react/no-multi-comp */
import { useIsFocused } from '@react-navigation/native';
import { Route, useIsFocused } from '@react-navigation/native';
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
@@ -9,7 +8,7 @@ import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
import { closeChat } from '../../actions.native';
import AbstractChat, {
type Props as AbstractProps,
IProps as AbstractProps,
_mapStateToProps
} from '../AbstractChat';
@@ -18,24 +17,24 @@ import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
import styles from './styles';
type Props = AbstractProps & {
interface IProps extends AbstractProps {
/**
* Default prop for navigating between screen components(React Navigation).
*/
navigation: Object,
navigation: any;
/**
* Default prop for navigating between screen components(React Navigation).
*/
route: Object
};
route: Route<'', { privateMessageRecipient: { name: string; }; }>;
}
/**
* Implements a React native component that renders the chat window (modal) of
* the mobile client.
*/
class Chat extends AbstractChat<Props> {
class Chat extends AbstractChat<IProps> {
/**
* Implements React's {@link Component#render()}.
*
@@ -51,17 +50,16 @@ class Chat extends AbstractChat<Props> {
hasBottomTextInput = { true }
hasTabNavigator = { true }
style = { styles.chatContainer }>
{/* @ts-ignore */}
<MessageContainer messages = { _messages } />
<MessageRecipient privateMessageRecipient = { privateMessageRecipient } />
<ChatInputBar onSend = { this._onSendMessage } />
</JitsiScreen>
);
}
_onSendMessage: (string) => void;
}
export default translate(connect(_mapStateToProps)(props => {
export default translate(connect(_mapStateToProps)((props: IProps) => {
const { _nbUnreadMessages, dispatch, navigation, t } = props;
const unreadMessagesNr = _nbUnreadMessages > 0;
@@ -78,7 +76,9 @@ export default translate(connect(_mapStateToProps)(props => {
)
});
return () => isFocused && dispatch(closeChat());
return () => {
isFocused && dispatch(closeChat());
};
}, [ isFocused, _nbUnreadMessages ]);
return (

View File

@@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { CHAT_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
@@ -10,23 +11,23 @@ import { screen } from '../../../mobile/navigation/routes';
import { getUnreadPollCount } from '../../../polls/functions';
import { getUnreadCount } from '../../functions';
type Props = AbstractButtonProps & {
interface IProps extends AbstractButtonProps {
/**
* True if the polls feature is disabled.
*/
_isPollsDisabled: boolean,
_isPollsDisabled?: boolean;
/**
* The unread message count.
*/
_unreadMessageCount: number
};
_unreadMessageCount: number;
}
/**
* Implements an {@link AbstractButton} to open the chat screen on mobile.
*/
class ChatButton extends AbstractButton<Props, *> {
class ChatButton extends AbstractButton<IProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.chat';
icon = IconMessage;
label = 'toolbar.chat';
@@ -60,9 +61,9 @@ class ChatButton extends AbstractButton<Props, *> {
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @returns {Props}
* @returns {IProps}
*/
function _mapStateToProps(state, ownProps) {
function _mapStateToProps(state: IReduxState, ownProps: any) {
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
const { disablePolls } = state['features/base/config'];
const { visible = enabled } = ownProps;

View File

@@ -25,10 +25,6 @@ class ChatPrivacyDialog extends AbstractChatPrivacyDialog {
onSubmit = { this._onSendPrivateMessage } />
);
}
_onSendGroupMessage: () => boolean;
_onSendPrivateMessage: () => boolean;
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPrivacyDialog));

View File

@@ -1,63 +1,65 @@
import React from 'react';
import { Text, TouchableHighlight, View } from 'react-native';
import { Text, TouchableHighlight, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconCloseLarge } from '../../../base/icons/svg';
import { ILocalParticipant } from '../../../base/participants/types';
import {
setParams
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { setLobbyChatActiveState, setPrivateMessageRecipient } from '../../actions.any';
import AbstractMessageRecipient, {
type Props as AbstractProps
IProps as AbstractProps
} from '../AbstractMessageRecipient';
import styles from './styles';
type Props = AbstractProps & {
interface IProps extends AbstractProps {
/**
* The Redux dispatch function.
*/
dispatch: Function,
dispatch: IStore['dispatch'];
/**
* Is lobby messaging active.
*/
isLobbyChatActive: boolean,
isLobbyChatActive: boolean;
/**
* The participant string for lobby chat messaging.
*/
lobbyMessageRecipient: Object,
lobbyMessageRecipient?: {
id: string;
name: string;
} | ILocalParticipant;
/**
* The participant object set for private messaging.
*/
privateMessageRecipient: Object,
};
privateMessageRecipient: { name: string; };
}
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient<Props> {
class MessageRecipient extends AbstractMessageRecipient<IProps> {
/**
* Constructor of the component.
*
* @param {Props} props - The props of the component.
* @param {IProps} props - The props of the component.
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this._onResetPrivateMessageRecipient = this._onResetPrivateMessageRecipient.bind(this);
this._onResetLobbyMessageRecipient = this._onResetLobbyMessageRecipient.bind(this);
}
_onResetLobbyMessageRecipient: () => void;
/**
* Resets lobby message recipient from state.
*
@@ -69,8 +71,6 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
dispatch(setLobbyChatActiveState(false));
}
_onResetPrivateMessageRecipient: () => void;
/**
* Resets private message recipient from state.
*
@@ -102,10 +102,10 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
if (isLobbyChatActive) {
return (
<View style = { styles.lobbyMessageRecipientContainer }>
<View style = { styles.lobbyMessageRecipientContainer as ViewStyle }>
<Text style = { styles.messageRecipientText }>
{ t('chat.lobbyChatMessageTo', {
recipient: lobbyMessageRecipient.name
recipient: lobbyMessageRecipient?.name
}) }
</Text>
<TouchableHighlight
@@ -123,7 +123,7 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
}
return (
<View style = { styles.messageRecipientContainer }>
<View style = { styles.messageRecipientContainer as ViewStyle }>
<Text style = { styles.messageRecipientText }>
{ t('chat.messageTo', {
recipient: privateMessageRecipient.name
@@ -145,9 +145,10 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
* @param {any} _ownProps - Component's own props.
* @returns {IProps}
*/
function _mapStateToProps(state) {
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
return {

View File

@@ -29,11 +29,11 @@ const styles = (theme: Theme) => {
chatMessage: {
display: 'inline-flex',
padding: '12px',
marginRight: '12px',
backgroundColor: theme.palette.ui02,
borderRadius: '4px 12px 12px 12px',
maxWidth: '100%',
marginTop: '4px',
boxSizing: 'border-box' as const,
'&.privatemessage': {
backgroundColor: theme.palette.support05
@@ -62,7 +62,8 @@ const styles = (theme: Theme) => {
replyWrapper: {
display: 'flex',
flexDirection: 'row' as const,
alignItems: 'center'
alignItems: 'center',
maxWidth: '100%'
},
messageContent: {
@@ -126,7 +127,7 @@ class ChatMessage extends AbstractChatMessage<IProps> {
return (
<div
className = { classes.chatMessageWrapper }
className = { clsx(classes.chatMessageWrapper, type) }
id = { this.props.message.messageId }
tabIndex = { -1 }>
<div

View File

@@ -25,7 +25,11 @@ const useStyles = makeStyles()(theme => {
messageGroup: {
display: 'flex',
flexDirection: 'column',
maxWidth: '100%'
maxWidth: '100%',
'&.remote': {
maxWidth: 'calc(100% - 40px)' // 100% - avatar and margin
}
},
groupContainer: {

View File

@@ -28,6 +28,13 @@ const useStyles = makeStyles()(theme => {
color: theme.palette.text01
},
text: {
maxWidth: 'calc(100% - 30px)',
overflow: 'hidden',
whiteSpace: 'break-spaces',
wordBreak: 'break-all'
},
iconButton: {
padding: '2px',
@@ -72,7 +79,7 @@ const MessageRecipient = ({
className = { classes.container }
id = 'chat-recipient'
role = 'alert'>
<span>
<span className = { classes.text }>
{t(_isLobbyChatActive ? 'chat.lobbyChatMessageTo' : 'chat.messageTo', {
recipient: _isLobbyChatActive ? _lobbyMessageRecipient : _privateMessageRecipient
})}

View File

@@ -12,6 +12,7 @@ import AbstractDialogTab, {
import { translate } from '../../base/i18n/functions';
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
import Checkbox from '../../base/ui/components/web/Checkbox';
import { iAmVisitor as iAmVisitorCheck } from '../../visitors/functions';
import logger from '../logger';
import AudioInputPreview from './AudioInputPreview';
@@ -92,6 +93,11 @@ interface IProps extends AbstractDialogTabProps, WithTranslation {
*/
hideNoiseSuppression: boolean;
/**
* Whether we are in visitors mode.
*/
iAmVisitor: boolean;
/**
* Wether noise suppression is on or not.
*/
@@ -234,6 +240,7 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
hideAudioOutputPreview,
hideDeviceHIDContainer,
hideNoiseSuppression,
iAmVisitor,
noiseSuppressionEnabled,
selectedAudioOutputId,
t
@@ -242,12 +249,12 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
return (
<div className = { classes.container }>
<div
{!iAmVisitor && <div
aria-live = 'polite'
className = { classes.inputContainer }>
{this._renderSelector(audioInput)}
</div>
{!hideAudioInputPreview && hasAudioPermission
</div>}
{!hideAudioInputPreview && hasAudioPermission && !iAmVisitor
&& <AudioInputPreview
track = { this.state.previewAudioTrack } />}
<div
@@ -259,7 +266,7 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
className = { classes.outputButton }
deviceId = { selectedAudioOutputId } />}
</div>
{!hideNoiseSuppression && (
{!hideNoiseSuppression && !iAmVisitor && (
<div className = { classes.noiseSuppressionContainer }>
<Checkbox
checked = { noiseSuppressionEnabled }
@@ -270,7 +277,7 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
}) } />
</div>
)}
{!hideDeviceHIDContainer
{!hideDeviceHIDContainer && !iAmVisitor
&& <DeviceHidContainer />}
</div>
);
@@ -380,7 +387,8 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
const mapStateToProps = (state: IReduxState) => {
return {
availableDevices: state['features/base/devices'].availableDevices ?? {}
availableDevices: state['features/base/devices'].availableDevices ?? {},
iAmVisitor: iAmVisitorCheck(state)
};
};

View File

@@ -27,15 +27,6 @@ export const HIDE_GIF_FOR_PARTICIPANT = 'HIDE_GIF_FOR_PARTICIPANT';
*/
export const REMOVE_GIF_FOR_PARTICIPANT = 'REMOVE_GIF_FOR_PARTICIPANT';
/**
* Set gif menu drawer visibility.
* {{
* type: SET_GIF_DRAWER_VISIBILITY,
* visible: boolean
* }}
*/
export const SET_GIF_DRAWER_VISIBILITY = 'SET_GIF_DRAWER_VISIBILITY';
/**
* Set gif menu visibility.
* {{

View File

@@ -2,7 +2,6 @@ import {
ADD_GIF_FOR_PARTICIPANT,
HIDE_GIF_FOR_PARTICIPANT,
REMOVE_GIF_FOR_PARTICIPANT,
SET_GIF_DRAWER_VISIBILITY,
SET_GIF_MENU_VISIBILITY,
SHOW_GIF_FOR_PARTICIPANT
} from './actionTypes';
@@ -61,19 +60,6 @@ export function hideGif(participantId: string) {
};
}
/**
* Set visibility of the GIF drawer.
*
* @param {boolean} visible - Whether or not it should be visible.
* @returns {Object}
*/
export function setGifDrawerVisibility(visible: boolean) {
return {
type: SET_GIF_DRAWER_VISIBILITY,
visible
};
}
/**
* Set visibility of the GIF menu.
*

View File

@@ -12,11 +12,10 @@ import Input from '../../../base/ui/components/web/Input';
import { sendMessage } from '../../../chat/actions.any';
import { SCROLL_SIZE } from '../../../filmstrip/constants';
import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
import { setOverflowMenuVisible } from '../../../toolbox/actions.web';
import { IReactionsMenuParent } from '../../../reactions/types';
import Drawer from '../../../toolbox/components/web/Drawer';
import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
import { showOverflowDrawer } from '../../../toolbox/functions.web';
import { setGifDrawerVisibility } from '../../actions';
import { setGifMenuVisibility } from '../../actions';
import {
formatGifUrlMessage,
getGifAPIKey,
@@ -60,10 +59,17 @@ const useStyles = makeStyles()(theme => {
marginTop: theme.spacing(1)
},
overflowMenu: {
overflowDrawerMenu: {
padding: theme.spacing(3),
width: '100%',
boxSizing: 'border-box'
boxSizing: 'border-box',
height: '100%'
},
overflowMenu: {
height: '200px',
width: '201px',
marginBottom: '0px'
},
gifContainerOverflow: {
@@ -77,19 +83,25 @@ const useStyles = makeStyles()(theme => {
};
});
interface IProps {
columns?: number;
parent: IReactionsMenuParent;
}
/**
* Gifs menu.
*
* @returns {ReactElement}
*/
function GifsMenu() {
function GifsMenu({ columns = 2, parent }: IProps) {
const API_KEY = useSelector(getGifAPIKey);
const giphyFetch = new GiphyFetch(API_KEY);
const [ searchKey, setSearchKey ] = useState<string>();
const { classes: styles, cx } = useStyles();
const dispatch = useDispatch();
const { t } = useTranslation();
const overflowDrawer: boolean = useSelector(showOverflowDrawer);
const isInOverflowMenu
= parent === IReactionsMenuParent.OverflowDrawer || parent === IReactionsMenuParent.OverflowMenu;
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const rating = useSelector(getGifRating);
const proxyUrl = useSelector(getGiphyProxyUrl);
@@ -109,8 +121,7 @@ function GifsMenu() {
}, [ searchKey ]);
const onDrawerClose = useCallback(() => {
dispatch(setGifDrawerVisibility(false));
dispatch(setOverflowMenuVisible(false));
dispatch(setGifMenuVisibility(false));
}, []);
const handleGifClick = useCallback((gif, e) => {
@@ -121,9 +132,9 @@ function GifsMenu() {
batch(() => {
dispatch(sendMessage(formatGifUrlMessage(url), true));
dispatch(toggleReactionsMenuVisibility());
overflowDrawer && onDrawerClose();
isInOverflowMenu && onDrawerClose();
});
}, [ dispatch, overflowDrawer ]);
}, [ dispatch, isInOverflowMenu ]);
const handleGifKeyPress = useCallback((gif, e) => {
if (e.nativeEvent.keyCode === 13) {
@@ -191,7 +202,8 @@ function GifsMenu() {
const gifMenu = (
<div
className = { cx(styles.gifsMenu,
overflowDrawer && styles.overflowMenu
parent === IReactionsMenuParent.OverflowDrawer && styles.overflowDrawerMenu,
parent === IReactionsMenuParent.OverflowMenu && styles.overflowMenu
) }>
<Input
autoFocus = { true }
@@ -208,9 +220,9 @@ function GifsMenu() {
value = { searchKey ?? '' } />
<div
className = { cx(styles.gifContainer,
overflowDrawer && styles.gifContainerOverflow) }>
parent === IReactionsMenuParent.OverflowDrawer && styles.gifContainerOverflow) }>
<Grid
columns = { 2 }
columns = { columns }
fetchGifs = { fetchGifs }
gutter = { 6 }
hideAttribution = { true }
@@ -219,9 +231,9 @@ function GifsMenu() {
noResultsMessage = { t('giphy.noResults') }
onGifClick = { handleGifClick }
onGifKeyPress = { handleGifKeyPress }
width = { overflowDrawer
width = { parent === IReactionsMenuParent.OverflowDrawer
? clientWidth - (2 * OVERFLOW_DRAWER_PADDING) - SCROLL_SIZE
: 320
: parent === IReactionsMenuParent.OverflowMenu ? 201 : 320
} />
</div>
<div className = { styles.logoContainer }>
@@ -233,7 +245,7 @@ function GifsMenu() {
</div>
);
return overflowDrawer ? (
return parent === IReactionsMenuParent.OverflowDrawer ? (
<JitsiPortal>
<Drawer
className = { styles.drawer }

View File

@@ -3,30 +3,29 @@ import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import ReactionButton from '../../../reactions/components/web/ReactionButton';
import { showOverflowDrawer } from '../../../toolbox/functions.web';
import { setGifDrawerVisibility, setGifMenuVisibility } from '../../actions';
import { IReactionsMenuParent } from '../../../reactions/types';
import { setGifMenuVisibility } from '../../actions';
import { isGifsMenuOpen } from '../../functions.web';
const GifsMenuButton = () => {
interface IProps {
parent: IReactionsMenuParent;
}
const GifsMenuButton = ({ parent }: IProps) => {
const menuOpen = useSelector(isGifsMenuOpen);
const overflowDrawer = useSelector(showOverflowDrawer);
const { t } = useTranslation();
const dispatch = useDispatch();
const icon = (
<img
alt = 'GIPHY Logo'
height = { 24 }
height = { parent === IReactionsMenuParent.OverflowMenu ? 16 : 24 }
src = 'images/GIPHY_icon.png' />
);
const handleClick = useCallback(() =>
dispatch(
overflowDrawer
? setGifDrawerVisibility(!menuOpen)
: setGifMenuVisibility(!menuOpen)
)
, [ menuOpen, overflowDrawer ]);
const handleClick = useCallback(() => {
dispatch(setGifMenuVisibility(!menuOpen));
}, [ menuOpen, parent ]);
return (
<ReactionButton

View File

@@ -1,5 +1,4 @@
import { IReduxState } from '../app/types';
import { showOverflowDrawer } from '../toolbox/functions.web';
export * from './function.any';
@@ -10,8 +9,5 @@ export * from './function.any';
* @returns {boolean}
*/
export function isGifsMenuOpen(state: IReduxState) {
const overflowDrawer = showOverflowDrawer(state);
const { drawerVisible, menuOpen } = state['features/gifs'];
return overflowDrawer ? drawerVisible : menuOpen;
return state['features/gifs'].menuOpen;
}

View File

@@ -4,12 +4,10 @@ import {
ADD_GIF_FOR_PARTICIPANT,
HIDE_GIF_FOR_PARTICIPANT,
REMOVE_GIF_FOR_PARTICIPANT,
SET_GIF_DRAWER_VISIBILITY,
SET_GIF_MENU_VISIBILITY
} from './actionTypes';
const initialState = {
drawerVisible: false,
gifList: new Map(),
menuOpen: false
};
@@ -20,7 +18,6 @@ export interface IGif {
}
export interface IGifsState {
drawerVisible: boolean;
gifList: Map<string, IGif>;
menuOpen: boolean;
}
@@ -66,11 +63,6 @@ ReducerRegistry.register<IGifsState>(
gifList: newList
};
}
case SET_GIF_DRAWER_VISIBILITY:
return {
...state,
drawerVisible: action.visible
};
case SET_GIF_MENU_VISIBILITY:
return {
...state,

View File

@@ -97,7 +97,7 @@ class DialInNumber extends Component<IProps> {
{ phoneNumber }
</span>
</span>
<span className = 'spacer'>&nbsp;</span>
<br />
<span className = 'conference-id'>
<span className = 'info-label'>
{ t('info.dialInConferenceID') }

View File

@@ -165,31 +165,31 @@ ReducerRegistry.register<IPrejoinState>(
function getStatusFromErrors(errors: {
audioAndVideoError?: { message: string; };
audioOnlyError?: { message: string; };
videoOnlyError?: Object; }
videoOnlyError?: { message: string; }; }
) {
const { audioOnlyError, videoOnlyError, audioAndVideoError } = errors;
if (audioAndVideoError) {
if (audioOnlyError) {
if (videoOnlyError) {
return {
deviceStatusType: 'warning',
deviceStatusText: 'prejoin.audioAndVideoError',
rawError: audioAndVideoError.message
};
}
return {
deviceStatusType: 'warning',
deviceStatusText: 'prejoin.audioAndVideoError',
rawError: audioAndVideoError.message
};
}
return {
deviceStatusType: 'warning',
deviceStatusText: 'prejoin.audioOnlyError',
rawError: audioOnlyError.message
};
}
if (audioOnlyError) {
return {
deviceStatusType: 'warning',
deviceStatusText: 'prejoin.audioOnlyError',
rawError: audioOnlyError.message
};
}
if (videoOnlyError) {
return {
deviceStatusType: 'warning',
deviceStatusText: 'prejoin.videoOnlyError',
rawError: audioAndVideoError.message
rawError: videoOnlyError.message
};
}

View File

@@ -101,9 +101,12 @@ class ReactionButton extends AbstractToolbarButton<IProps, IState> {
/**
* Handles reaction button click.
*
* @param {Event} event - The click event.
* @returns {void}
*/
_onClickHandler() {
_onClickHandler(event: any) {
event.preventDefault();
event.stopPropagation();
this.props.onClick();
clearTimeout(this.state.increaseTimeout ?? 0);
const timeout = window.setTimeout(() => {

View File

@@ -6,7 +6,6 @@ import { makeStyles } from 'tss-react/mui';
import { createReactionMenuEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState, IStore } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { raiseHand } from '../../../base/participants/actions';
import { getLocalParticipant, hasRaisedHand } from '../../../base/participants/functions';
import GifsMenu from '../../../gifs/components/web/GifsMenu';
@@ -15,7 +14,13 @@ import { isGifEnabled, isGifsMenuOpen } from '../../../gifs/functions';
import { dockToolbox } from '../../../toolbox/actions.web';
import { addReactionToBuffer } from '../../actions.any';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { REACTIONS, REACTIONS_MENU_HEIGHT } from '../../constants';
import {
GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU,
RAISE_HAND_ROW_HEIGHT, REACTIONS,
REACTIONS_MENU_HEIGHT_DRAWER,
REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU
} from '../../constants';
import { IReactionsMenuParent } from '../../types';
import ReactionButton from './ReactionButton';
@@ -36,11 +41,6 @@ interface IProps {
*/
_isGifMenuVisible: boolean;
/**
* Whether or not it's a mobile browser.
*/
_isMobile: boolean;
/**
* The ID of the local participant.
*/
@@ -57,13 +57,59 @@ interface IProps {
dispatch: IStore['dispatch'];
/**
* Whether or not it's displayed in the overflow menu.
* Indicates the parent of the reactions menu.
*/
overflowMenu?: boolean;
parent: IReactionsMenuParent;
/**
* Whether to show the raised hand button.
*/
showRaisedHand?: boolean;
}
const useStyles = makeStyles()(theme => {
const useStyles = makeStyles<IProps>()((theme, props: IProps) => {
const { parent, showRaisedHand, _isGifMenuVisible } = props;
let reactionsMenuHeight = REACTIONS_MENU_HEIGHT_DRAWER;
if (parent === IReactionsMenuParent.OverflowDrawer || parent === IReactionsMenuParent.OverflowMenu) {
if (parent === IReactionsMenuParent.OverflowMenu) {
reactionsMenuHeight = REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU;
if (_isGifMenuVisible) {
reactionsMenuHeight += GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU;
}
}
if (!showRaisedHand) {
reactionsMenuHeight -= RAISE_HAND_ROW_HEIGHT;
}
}
return {
reactionsMenuInOverflowMenu: {
'&.reactions-menu': {
'&.with-gif': {
width: 'inherit'
},
'.reactions-row': {
'.toolbox-icon': {
width: '24px',
height: '24px',
'span.emoji': {
width: '24px',
height: '24px',
lineHeight: '24px',
fontSize: '16px'
}
}
},
'.raise-hand-row': {
'.toolbox-icon': {
height: '32px'
}
}
}
},
overflow: {
width: 'auto',
paddingBottom: 'max(env(safe-area-inset-bottom, 0), 16px)',
@@ -72,21 +118,55 @@ const useStyles = makeStyles()(theme => {
borderRadius: 0,
position: 'relative',
boxSizing: 'border-box',
height: `${REACTIONS_MENU_HEIGHT}px`
height: `${reactionsMenuHeight}px`
}
};
});
const ReactionsMenu = ({
_dockToolbox,
_isGifEnabled,
_isGifMenuVisible,
_isMobile,
_raisedHand,
dispatch,
overflowMenu
}: IProps) => {
const { classes, cx } = useStyles();
const _getReactionButtons = (dispatch: IStore['dispatch'], t: Function) => {
let modifierKey = 'Alt';
if (window.navigator?.platform) {
if (window.navigator.platform.indexOf('Mac') !== -1) {
modifierKey = '⌥';
}
}
return Object.keys(REACTIONS).map(key => {
/**
* Sends reaction message.
*
* @returns {void}
*/
function doSendReaction() {
dispatch(addReactionToBuffer(key));
sendAnalytics(createReactionMenuEvent(key));
}
return (<ReactionButton
accessibilityLabel = { t(`toolbar.accessibilityLabel.${key}`) }
icon = { REACTIONS[key].emoji }
key = { key }
// eslint-disable-next-line react/jsx-no-bind
onClick = { doSendReaction }
toggled = { false }
tooltip = { `${t(`toolbar.${key}`)} (${modifierKey} + ${REACTIONS[key].shortcutChar})` } />);
});
};
const ReactionsMenu = (props: IProps) => {
const {
_dockToolbox,
_isGifEnabled,
_isGifMenuVisible,
_raisedHand,
dispatch,
parent,
showRaisedHand = false
} = props;
const isInOverflowMenu
= parent === IReactionsMenuParent.OverflowDrawer || parent === IReactionsMenuParent.OverflowMenu;
const { classes, cx } = useStyles(props);
const { t } = useTranslation();
useEffect(() => {
@@ -97,9 +177,9 @@ const ReactionsMenu = ({
};
}, []);
const _doToggleRaiseHand = () => {
const _doToggleRaiseHand = useCallback(() => {
dispatch(raiseHand(!_raisedHand));
};
}, [ _raisedHand ]);
const _onToolbarToggleRaiseHand = useCallback(() => {
sendAnalytics(createToolbarEvent(
@@ -109,47 +189,26 @@ const ReactionsMenu = ({
dispatch(toggleReactionsMenuVisibility());
}, [ _raisedHand ]);
const _getReactionButtons = () => {
let modifierKey = 'Alt';
const buttons = _getReactionButtons(dispatch, t);
if (window.navigator?.platform) {
if (window.navigator.platform.indexOf('Mac') !== -1) {
modifierKey = '⌥';
}
}
return Object.keys(REACTIONS).map(key => {
/**
* Sends reaction message.
*
* @returns {void}
*/
function doSendReaction() {
dispatch(addReactionToBuffer(key));
sendAnalytics(createReactionMenuEvent(key));
}
return (<ReactionButton
accessibilityLabel = { t(`toolbar.accessibilityLabel.${key}`) }
icon = { REACTIONS[key].emoji }
key = { key }
// eslint-disable-next-line react/jsx-no-bind
onClick = { doSendReaction }
toggled = { false }
tooltip = { `${t(`toolbar.${key}`)} (${modifierKey} + ${REACTIONS[key].shortcutChar})` } />);
});
};
if (_isGifEnabled) {
buttons.push(<GifsMenuButton parent = { parent } />);
}
return (
<div
className = { cx('reactions-menu', _isGifEnabled && 'with-gif',
overflowMenu && `overflow ${classes.overflow}`) }>
{_isGifEnabled && _isGifMenuVisible && <GifsMenu />}
className = { cx('reactions-menu',
parent === IReactionsMenuParent.OverflowMenu && classes.reactionsMenuInOverflowMenu,
_isGifEnabled && 'with-gif',
isInOverflowMenu && `overflow ${classes.overflow}`) }>
{_isGifEnabled && _isGifMenuVisible
&& <GifsMenu
columns = { parent === IReactionsMenuParent.OverflowMenu ? 1 : undefined }
parent = { parent } />}
<div className = 'reactions-row'>
{_getReactionButtons()}
{_isGifEnabled && <GifsMenuButton />}
{ buttons }
</div>
{_isMobile && (
{showRaisedHand && (
<div className = 'raise-hand-row'>
<ReactionButton
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
@@ -157,7 +216,7 @@ const ReactionsMenu = ({
key = 'raisehand'
label = {
`${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
${overflowMenu ? '' : ' (R)'}`
${isInOverflowMenu ? '' : ' (R)'}`
}
onClick = { _onToolbarToggleRaiseHand }
toggled = { true } />
@@ -175,11 +234,9 @@ const ReactionsMenu = ({
*/
function mapStateToProps(state: IReduxState) {
const localParticipant = getLocalParticipant(state);
const { isNarrowLayout } = state['features/base/responsive-ui'];
return {
_localParticipantID: localParticipant?.id,
_isMobile: isMobileBrowser() || isNarrowLayout,
_isGifEnabled: isGifEnabled(state),
_isGifMenuVisible: isGifsMenuOpen(state),
_raisedHand: hasRaisedHand(localParticipant)

View File

@@ -1,16 +1,18 @@
import React, { useCallback } from 'react';
import React, { ReactElement, useCallback } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { IconArrowUp } from '../../../base/icons/svg';
import ToolboxButtonWithIconPopup from '../../../base/toolbox/components/web/ToolboxButtonWithIconPopup';
import { IconArrowUp, IconFaceSmile } from '../../../base/icons/svg';
import AbstractButton, { type IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import ToolboxButtonWithPopup from '../../../base/toolbox/components/web/ToolboxButtonWithPopup';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { IReactionEmojiProps } from '../../constants';
import { getReactionsQueue, isReactionsEnabled } from '../../functions.any';
import { getReactionsMenuVisibility } from '../../functions.web';
import { getReactionsMenuVisibility, isReactionsButtonEnabled } from '../../functions.web';
import { IReactionsMenuParent } from '../../types';
import RaiseHandButton from './RaiseHandButton';
import ReactionEmoji from './ReactionEmoji';
@@ -19,9 +21,14 @@ import ReactionsMenu from './ReactionsMenu';
interface IProps extends WithTranslation {
/**
* Whether or not reactions are enabled.
* Whether a mobile browser is used or not.
*/
_reactionsEnabled: Boolean;
_isMobile: boolean;
/**
* Whether the reactions should be displayed on separate button or not.
*/
_reactionsButtonEnabled: boolean;
/**
* The button's key.
@@ -60,13 +67,28 @@ interface IProps extends WithTranslation {
reactionsQueue: Array<IReactionEmojiProps>;
}
/**
* Implementation of a button for reactions.
*/
class ReactionsButtonImpl extends AbstractButton<AbstractButtonProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.reactions';
icon = IconFaceSmile;
label = 'toolbar.reactions';
toggledLabel = 'toolbar.reactions';
tooltip = 'toolbar.reactions';
}
const ReactionsButton = translate(connect()(ReactionsButtonImpl));
/**
* Button used for the reactions menu.
*
* @returns {ReactElement}
*/
function ReactionsMenuButton({
_reactionsEnabled,
_reactionsButtonEnabled,
_isMobile,
buttonKey,
dispatch,
handleClick,
@@ -85,43 +107,69 @@ function ReactionsMenuButton({
!visible && toggleReactionsMenu();
}, [ visible, toggleReactionsMenu ]);
const closeReactionsMenu = useCallback(() => {
visible && toggleReactionsMenu();
}, [ visible, toggleReactionsMenu ]);
const reactionsMenu = (<div className = 'reactions-menu-container'>
<ReactionsMenu />
<ReactionsMenu parent = { IReactionsMenuParent.Button } />
</div>);
return (
<div className = 'reactions-menu-popup-container'>
{!_reactionsEnabled || isNarrow ? (
let content: ReactElement | null = null;
if (_reactionsButtonEnabled) {
content = (
<ToolboxButtonWithPopup
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
onPopoverClose = { _isMobile ? closeReactionsMenu : toggleReactionsMenu }
onPopoverOpen = { openReactionsMenu }
popoverContent = { reactionsMenu }
trigger = { _isMobile ? 'click' : undefined }
visible = { visible }>
<ReactionsButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />
</ToolboxButtonWithPopup>);
} else {
content = isNarrow
? (
<RaiseHandButton
buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />)
: (
<ToolboxButtonWithIconPopup
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
icon = { IconArrowUp }
iconDisabled = { false }
iconId = 'reactions-menu-button'
onPopoverClose = { toggleReactionsMenu }
onPopoverOpen = { openReactionsMenu }
popoverContent = { reactionsMenu }
visible = { visible }>
<RaiseHandButton
buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />
</ToolboxButtonWithIconPopup>
)}
: (
<ToolboxButtonWithPopup
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
icon = { IconArrowUp }
iconDisabled = { false }
iconId = 'reactions-menu-button'
onPopoverClose = { toggleReactionsMenu }
onPopoverOpen = { openReactionsMenu }
popoverContent = { reactionsMenu }
visible = { visible }>
<RaiseHandButton
buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />
</ToolboxButtonWithPopup>);
}
return (
<div className = 'reactions-menu-popup-container'>
{ content }
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>
);
</div>);
}
/**
@@ -134,9 +182,11 @@ function mapStateToProps(state: IReduxState) {
const { isNarrowLayout } = state['features/base/responsive-ui'];
return {
_reactionsButtonEnabled: isReactionsButtonEnabled(state),
_reactionsEnabled: isReactionsEnabled(state),
_isMobile: isMobileBrowser(),
isOpen: getReactionsMenuVisibility(state),
isNarrow: isMobileBrowser() || isNarrowLayout,
isNarrow: isNarrowLayout,
reactionsQueue: getReactionsQueue(state)
};
}

View File

@@ -8,9 +8,24 @@ import {
} from './sounds';
/**
* Reactions menu height on mobile web (px).
* The height of the raise hand row in the reactions menu.
*/
export const REACTIONS_MENU_HEIGHT = 144;
export const RAISE_HAND_ROW_HEIGHT = 54;
/**
* The height of the gifs menu when displayed as part of the overflow menu.
*/
export const GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU = 200;
/**
* Reactions menu height when displayed as part of drawer.
*/
export const REACTIONS_MENU_HEIGHT_DRAWER = 144;
/**
* Reactions menu height when displayed as part of overflow menu.
*/
export const REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU = 106;
/**
* The payload name for the datachannel/endpoint reaction event.

View File

@@ -1,4 +1,7 @@
import { IReduxState } from '../app/types';
import { getToolbarButtons } from '../base/config/functions.web';
import { isReactionsEnabled } from './functions.any';
export * from './functions.any';
@@ -11,3 +14,13 @@ export * from './functions.any';
export function getReactionsMenuVisibility(state: IReduxState): boolean {
return state['features/reactions'].visible;
}
/**
* Whether or not the reactions button is enabled.
*
* @param {Object} state - The Redux state object.
* @returns {boolean}
*/
export function isReactionsButtonEnabled(state: IReduxState) {
return Boolean(getToolbarButtons(state).includes('reactions')) && isReactionsEnabled(state);
}

View File

@@ -0,0 +1,5 @@
export enum IReactionsMenuParent {
Button = 1,
OverflowMenu = 2,
OverflowDrawer = 3
}

View File

@@ -9,7 +9,7 @@ import { translate } from '../../../../base/i18n/functions';
import { navigate }
from '../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../../mobile/navigation/routes';
import { IProps } from '../../LiveStream/AbstractStartLiveStreamDialog';
import { IProps, _mapStateToProps as abstractMapStateToProps } from '../../LiveStream/AbstractStartLiveStreamDialog';
import AbstractRecordButton, {
IProps as AbstractProps,
_mapStateToProps as _abstractMapStateToProps
@@ -58,6 +58,7 @@ export function mapStateToProps(state: IReduxState) {
return {
...abstractProps,
...abstractMapStateToProps(state),
visible: enabled && iosEnabled && abstractProps.visible
};
}

View File

@@ -1,8 +1,6 @@
// @flow
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Text, TouchableHighlight, View } from 'react-native';
import { GestureResponderEvent, Text, TextStyle, TouchableHighlight, View, ViewStyle } from 'react-native';
import Icon from '../../../base/icons/components/Icon';
import { RECORD_TYPE } from '../../constants';
@@ -12,69 +10,71 @@ import styles from './styles';
/**
* The type of the React {@code Component} props of {@link RecordItem}.
*/
type Props = {
interface IProps {
/**
* The id of the record.
*/
id: String,
id?: string;
/**
* The name of the record.
*/
name: String,
name?: string;
/**
* The handler for the click event.
*/
onClick: Function,
onClick?: (e?: GestureResponderEvent | React.MouseEvent) => void;
/**
* The type of the record.
*/
type: String
type?: string;
}
/**
* Component to render Record data.
*
* @param {Props} props - The props of the component.
* @param {IProps} props - The props of the component.
* @returns {React$Element<any>}
*/
export const RecordItem = ({
id,
name,
type,
/* eslint-disable-next-line no-empty-function */
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
onClick = () => {}
}: Props) => {
}: IProps) => {
const { t } = useTranslation();
const IconRecord = RECORD_TYPE[type].icon;
const IconRecord = RECORD_TYPE[type ?? ''].icon;
return (
<TouchableHighlight onPress = { onClick }>
<View
key = { `record-${id}` }
style = { styles.recordItem }
style = { styles.recordItem as ViewStyle }
// @ts-ignore
title = { name }>
<View style = { styles.recordTypeIcon }>
<View style = { styles.recordTypeIcon as ViewStyle }>
{IconRecord && (
<Icon
src = { IconRecord }
style = { styles.recordIcon } />
)}
</View>
<View style = { styles.recordDetails }>
<View style = { styles.recordDetails as ViewStyle }>
<Text
key = { name }
numberOfLines = { 1 }
style = { styles.recordName }>
style = { styles.recordName as TextStyle }>
{name}
</Text>
<Text
key = { type }
style = { styles.recordType }>
{t(RECORD_TYPE[type].label)}
{t(RECORD_TYPE[type ?? ''].label)}
</Text>
</View>
</View>

View File

@@ -1,8 +1,9 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Platform, SafeAreaView, ScrollView, Text, View } from 'react-native';
import { Platform, SafeAreaView, ScrollView, Text, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IconSearch } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
@@ -24,7 +25,7 @@ import styles from './styles';
*/
const SalesforceLinkDialog = () => {
const { t } = useTranslation();
const { clientHeight } = useSelector(state => state['features/base/responsive-ui']);
const { clientHeight } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const {
hasDetailsErrors,
hasRecordsErrors,
@@ -48,7 +49,7 @@ const SalesforceLinkDialog = () => {
}, [ navigate, linkMeeting ]);
const renderSpinner = () => (
<View style = { [ styles.recordsSpinner, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] }>
<View style = { [ styles.recordsSpinner, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] as ViewStyle[] }>
<LoadingIndicator />
</View>
);
@@ -63,8 +64,8 @@ const SalesforceLinkDialog = () => {
<SafeAreaView>
<ScrollView
bounces = { false }
style = { [ styles.selectedRecord, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] }>
<View style = { styles.recordInfo }>
style = { [ styles.selectedRecord, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] as ViewStyle[] }>
<View style = { styles.recordInfo as ViewStyle }>
<RecordItem { ...selectedRecord } />
{ selectedRecordOwner && <RecordItem { ...selectedRecordOwner } /> }
{ hasDetailsErrors && renderDetailsErrors() }
@@ -75,9 +76,9 @@ const SalesforceLinkDialog = () => {
<Input
customStyles = {{ container: styles.notes }}
maxLength = { NOTES_MAX_LENGTH }
minHeight = { Platform.OS === 'ios' && NOTES_LINES ? 20 * NOTES_LINES : null }
minHeight = { Platform.OS === 'ios' && NOTES_LINES ? 20 * NOTES_LINES : undefined }
multiline = { true }
numberOfLines = { Platform.OS === 'ios' ? null : NOTES_LINES }
numberOfLines = { Platform.OS === 'ios' ? undefined : NOTES_LINES }
/* eslint-disable-next-line react/jsx-no-bind */
onChange = { value => setNotes(value) }
placeholder = { t('dialog.addMeetingNote') }
@@ -87,14 +88,14 @@ const SalesforceLinkDialog = () => {
);
const renderRecordsSearch = () => (
<View style = { styles.recordsSearchContainer }>
<View style = { styles.recordsSearchContainer as ViewStyle }>
<Input
icon = { IconSearch }
maxLength = { NOTES_MAX_LENGTH }
/* eslint-disable-next-line react/jsx-no-bind */
onChange = { value => setSearchTerm(value) }
placeholder = { t('dialog.searchInSalesforce') }
value = { searchTerm } />
value = { searchTerm ?? '' } />
{(!isLoading && !hasRecordsErrors) && (
<Text style = { styles.resultLabel }>
{showSearchResults
@@ -107,7 +108,7 @@ const SalesforceLinkDialog = () => {
);
const renderNoRecords = () => showNoResults && (
<View style = { [ styles.noRecords, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] }>
<View style = { [ styles.noRecords, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] as ViewStyle[] }>
<Text style = { styles.noRecordsText }>
{t('dialog.searchResultsNotFound')}
</Text>
@@ -118,7 +119,7 @@ const SalesforceLinkDialog = () => {
);
const renderRecordsError = () => (
<View style = { [ styles.recordsError, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] }>
<View style = { [ styles.recordsError, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] as ViewStyle[] }>
<Text style = { styles.recordsErrorText }>
{t('dialog.searchResultsError')}
</Text>
@@ -143,8 +144,8 @@ const SalesforceLinkDialog = () => {
<SafeAreaView>
<ScrollView
bounces = { false }
style = { [ styles.recordList, { height: clientHeight - LIST_HEIGHT_OFFSET } ] }>
{records.map(item => (
style = { [ styles.recordList, { height: clientHeight - LIST_HEIGHT_OFFSET } ] as ViewStyle[] }>
{records.map((item: any) => (
<RecordItem
key = { `record-${item.id}` }
/* eslint-disable-next-line react/jsx-no-bind */
@@ -164,7 +165,7 @@ const SalesforceLinkDialog = () => {
</View>
{
selectedRecord
&& <View style = { styles.footer }>
&& <View>
<Button
labelKey = 'dialog.Cancel'
/* eslint-disable-next-line react/jsx-no-bind */

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { GestureResponderEvent } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../app/types';
@@ -21,7 +22,7 @@ import {
interface ISelectedRecord {
id: string;
name: string;
onClick: (e?: React.MouseEvent) => void;
onClick: (e?: React.MouseEvent | GestureResponderEvent) => void;
type: string;
}

View File

@@ -1,11 +1,14 @@
import React, { PureComponent } from 'react';
import {
Text,
View
TextStyle,
View,
ViewStyle
} from 'react-native';
import { connect } from 'react-redux';
import type { Dispatch } from 'redux';
import { IReduxState, IStore } from '../../../../app/types';
import { IJitsiConference } from '../../../../base/conference/reducer';
import { getSecurityUiConfig } from '../../../../base/config/functions.any';
import { MEETING_PASSWORD_ENABLED } from '../../../../base/flags/constants';
import { getFeatureFlag } from '../../../../base/flags/functions';
@@ -40,94 +43,94 @@ const _TEXT_INPUT_PROPS = {
/**
* The type of the React {@code Component} props of {@link SecurityDialog}.
*/
type Props = {
interface IProps {
/**
* The JitsiConference which requires a password.
*/
_conference: Object,
_conference?: IJitsiConference;
/**
* Whether the local user is the moderator.
*/
_isModerator: boolean,
_isModerator: boolean;
/**
* State of the lobby mode.
*/
_lobbyEnabled: boolean,
_lobbyEnabled: boolean;
/**
* Whether the lobby mode switch is available or not.
*/
_lobbyModeSwitchVisible: boolean,
_lobbyModeSwitchVisible: boolean;
/**
* The value for how the conference is locked (or undefined if not locked)
* as defined by room-lock constants.
*/
_locked: string,
_locked?: string;
/**
* Checks if the conference room is locked or not.
*/
_lockedConference: boolean,
_lockedConference: boolean;
/**
* The current known password for the JitsiConference.
*/
_password: string,
_password?: string;
/**
* Number of digits used in the room-lock password.
*/
_passwordNumberOfDigits: number,
_passwordNumberOfDigits?: number;
/**
* Whether setting a room password is available or not.
*/
_roomPasswordControls: boolean,
_roomPasswordControls: boolean;
/**
* Redux store dispatch function.
*/
dispatch: Dispatch<any>,
dispatch: IStore['dispatch'];
/**
* Invoked to obtain translated strings.
*/
t: Function
};
t: Function;
}
/**
* The type of the React {@code Component} state of {@link SecurityDialog}.
*/
type State = {
interface IState {
/**
* Password added by the participant for room lock.
*/
passwordInputValue: string,
passwordInputValue: string;
/**
* Shows an input or a message.
*/
showElement: boolean
};
showElement: boolean;
}
/**
* Component that renders the security options dialog.
*
* @returns {React$Element<any>}
*/
class SecurityDialog extends PureComponent<Props, State> {
class SecurityDialog extends PureComponent<IProps, IState> {
/**
* Instantiates a new {@code SecurityDialog}.
*
* @inheritdoc
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this.state = {
@@ -180,8 +183,8 @@ class SecurityDialog extends PureComponent<Props, State> {
<Text style = { styles.lobbyModeText }>
{ t('lobby.enableDialogText') }
</Text>
<View style = { styles.lobbyModeSection }>
<Text style = { styles.lobbyModeLabel } >
<View style = { styles.lobbyModeSection as ViewStyle }>
<Text style = { styles.lobbyModeLabel as TextStyle } >
{ t('lobby.toggleLabel') }
</Text>
<Switch
@@ -267,7 +270,7 @@ class SecurityDialog extends PureComponent<Props, State> {
if (_locked === LOCKED_REMOTELY) {
if (_isModerator) {
setPasswordControls = (
<View style = { styles.passwordSetRemotelyContainer }>
<View style = { styles.passwordSetRemotelyContainer as ViewStyle }>
<Text style = { styles.passwordSetRemotelyText }>
{ t('passwordSetRemotely') }
</Text>
@@ -281,7 +284,7 @@ class SecurityDialog extends PureComponent<Props, State> {
);
} else {
setPasswordControls = (
<View style = { styles.passwordSetRemotelyContainer }>
<View style = { styles.passwordSetRemotelyContainer as ViewStyle }>
<Text style = { styles.passwordSetRemotelyTextDisabled }>
{ t('passwordSetRemotely') }
</Text>
@@ -306,7 +309,7 @@ class SecurityDialog extends PureComponent<Props, State> {
<View
style = {
_locked !== LOCKED_REMOTELY
&& styles.passwordContainerControls
&& styles.passwordContainerControls as ViewStyle
}>
<View>
{ this._setRoomPasswordMessage() }
@@ -324,7 +327,7 @@ class SecurityDialog extends PureComponent<Props, State> {
* @private
*/
_setRoomPasswordMessage() {
let textInputProps = _TEXT_INPUT_PROPS;
let textInputProps: any = _TEXT_INPUT_PROPS;
const {
_isModerator,
_locked,
@@ -362,8 +365,8 @@ class SecurityDialog extends PureComponent<Props, State> {
} else if (_locked) {
if (_locked === LOCKED_LOCALLY && typeof _password !== 'undefined') {
return (
<View style = { styles.savedPasswordContainer }>
<Text style = { styles.savedPasswordLabel }>
<View style = { styles.savedPasswordContainer as ViewStyle }>
<Text style = { styles.savedPasswordLabel as TextStyle }>
{ t('info.password') }
</Text>
<Text style = { styles.savedPassword }>
@@ -376,8 +379,6 @@ class SecurityDialog extends PureComponent<Props, State> {
}
}
_onToggleLobbyMode: () => void;
/**
* Handles the enable-disable lobby mode switch.
*
@@ -394,8 +395,6 @@ class SecurityDialog extends PureComponent<Props, State> {
}
}
_onAddPassword: () => void;
/**
* Callback to be invoked when add password button is pressed.
*
@@ -431,15 +430,13 @@ class SecurityDialog extends PureComponent<Props, State> {
return true;
}
_onChangeText: string => void;
/**
* Callback to be invoked when the text in the field changes.
*
* @param {string} passwordInputValue - The value of password input.
* @returns {void}
*/
_onChangeText(passwordInputValue) {
_onChangeText(passwordInputValue: string) {
if (!this._validateInputValue(passwordInputValue)) {
return;
}
@@ -449,8 +446,6 @@ class SecurityDialog extends PureComponent<Props, State> {
});
}
_onCancel: () => void;
/**
* Cancels value typed in text input.
*
@@ -465,8 +460,6 @@ class SecurityDialog extends PureComponent<Props, State> {
this.props.dispatch(unlockRoom());
}
_onCopy: () => void;
/**
* Copies room password.
*
@@ -478,8 +471,6 @@ class SecurityDialog extends PureComponent<Props, State> {
copyText(passwordInputValue);
}
_onSubmit: () => void;
/**
* Submits value typed in text input.
*
@@ -492,7 +483,7 @@ class SecurityDialog extends PureComponent<Props, State> {
} = this.props;
const { passwordInputValue } = this.state;
dispatch(endRoomLockRequest(_conference, passwordInputValue));
_conference && dispatch(endRoomLockRequest(_conference, passwordInputValue));
}
}
@@ -500,14 +491,14 @@ class SecurityDialog extends PureComponent<Props, State> {
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
* @returns {IProps}
*/
function _mapStateToProps(state: Object): Object {
function _mapStateToProps(state: IReduxState) {
const { conference, locked, password } = state['features/base/conference'];
const { disableLobbyPassword, hideLobbyButton } = getSecurityUiConfig(state);
const { lobbyEnabled } = state['features/lobby'];
const { roomPasswordNumberOfDigits } = state['features/base/config'];
const lobbySupported = conference && conference.isLobbySupported();
const lobbySupported = conference?.isLobbySupported();
const visible = getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true);
return {

View File

@@ -1,21 +1,17 @@
// @flow
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n/functions';
import { navigate } from '../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../../mobile/navigation/routes';
import AbstractSecurityDialogButton, {
type Props as AbstractSecurityDialogButtonProps,
IProps as AbstractSecurityDialogButtonProps,
_mapStateToProps as _abstractMapStateToProps
} from '../AbstractSecurityDialogButton';
type Props = AbstractSecurityDialogButtonProps;
/**
* Implements an {@link AbstractSecurityDialogButton} to open the security screen.
*/
class SecurityDialogButton<P: Props, S:*> extends AbstractSecurityDialogButton<P, S> {
class SecurityDialogButton<P extends AbstractSecurityDialogButtonProps, S> extends AbstractSecurityDialogButton<P, S> {
/**
* Opens / closes the security screen.

View File

@@ -1,5 +1,3 @@
// @flow
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
/**

View File

@@ -1,7 +1,6 @@
// @flow
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { WithTranslation } from 'react-i18next';
import { Text, View, ViewStyle } from 'react-native';
import { translate } from '../../../base/i18n/functions';
@@ -10,45 +9,40 @@ import styles, { ANDROID_UNDERLINE_COLOR, PLACEHOLDER_COLOR } from './styles';
/**
* The type of the React {@code Component} props of {@link FormRow}.
*/
type Props = {
interface IProps extends WithTranslation {
/**
*
* Component's children.
*/
children: Object,
children: React.ReactElement;
/**
* Prop to decide if a row separator is to be rendered.
*/
fieldSeparator: boolean,
fieldSeparator?: boolean;
/**
* The i18n key of the text label of the form field.
*/
label: string,
label: string;
/**
* One of 'row' (default) or 'column'.
*/
layout: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
layout?: string;
}
/**
* Implements a React {@code Component} which renders a standardized row on a
* form. The component should have exactly one child component.
*/
class FormRow extends Component<Props> {
class FormRow extends Component<IProps> {
/**
* Initializes a new {@code FormRow} instance.
*
* @param {Object} props - Component properties.
*/
constructor(props) {
constructor(props: IProps) {
super(props);
React.Children.only(this.props.children);
@@ -77,7 +71,7 @@ class FormRow extends Component<Props> {
return (
<View
style = { this._getRowStyle() } >
<View style = { styles.fieldLabelContainer } >
<View style = { styles.fieldLabelContainer as ViewStyle } >
<Text
style = { [
styles.text,
@@ -87,15 +81,13 @@ class FormRow extends Component<Props> {
{ t(this.props.label) }
</Text>
</View>
<View style = { styles.fieldValueContainer } >
<View style = { styles.fieldValueContainer as ViewStyle } >
{ newChild }
</View>
</View>
);
}
_getDefaultFieldProps: (field: Component<*, *>) => Object;
/**
* Assembles the default props to the field child component of this form
* row.
@@ -108,8 +100,8 @@ class FormRow extends Component<Props> {
* @private
* @returns {Object}
*/
_getDefaultFieldProps(field: Object) {
if (field && field.type) {
_getDefaultFieldProps(field?: React.ReactElement) {
if (field?.type) { // @ts-ignore
switch (field.type.displayName) {
case 'TextInput':
return {
@@ -126,8 +118,6 @@ class FormRow extends Component<Props> {
return {};
}
_getRowStyle: () => Array<Object>;
/**
* Assembles the row style array based on the row's props.
*
@@ -136,8 +126,8 @@ class FormRow extends Component<Props> {
*/
_getRowStyle() {
const { fieldSeparator, layout } = this.props;
const rowStyle = [
styles.fieldContainer
const rowStyle: ViewStyle[] = [
styles.fieldContainer as ViewStyle
];
if (fieldSeparator) {
@@ -146,7 +136,7 @@ class FormRow extends Component<Props> {
if (layout === 'column') {
rowStyle.push(
styles.fieldContainerColumn
styles.fieldContainerColumn as ViewStyle
);
}

View File

@@ -4,7 +4,6 @@ import { Text, View } from 'react-native';
import { translate } from '../../../base/i18n/functions';
// @ts-ignore
import styles from './styles';

View File

@@ -29,7 +29,6 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import { AVATAR_SIZE } from '../../../welcome/components/styles';
import { isServerURLChangeEnabled, normalizeUserInputURL } from '../../functions.native';
// @ts-ignore
import FormRow from './FormRow';
import FormSection from './FormSection';
import styles from './styles';

View File

@@ -69,6 +69,11 @@ export interface IProps extends AbstractDialogTabProps, WithTranslation {
*/
hideSelfView: boolean;
/**
* Whether we are in visitors mode.
*/
iAmVisitor: boolean;
/**
* The id of the local participant.
*/
@@ -243,6 +248,7 @@ class ProfileTab extends AbstractDialogTab<IProps, any> {
email,
hideEmailInSettings,
hideSelfView,
iAmVisitor,
id,
readOnlyName,
showLanguageSettings,
@@ -277,7 +283,7 @@ class ProfileTab extends AbstractDialogTab<IProps, any> {
type = 'text'
value = { email } />
</div>}
{!disableHideSelfView && (
{!disableHideSelfView && !iAmVisitor && (
<Checkbox
checked = { hideSelfView }
className = { classes.bottomMargin }

View File

@@ -24,6 +24,7 @@ import {
getVideoDeviceSelectionDialogProps
} from '../../../device-selection/functions.web';
import { checkBlurSupport } from '../../../virtual-background/functions';
import { iAmVisitor } from '../../../visitors/functions';
import {
submitModeratorTab,
submitMoreTab,
@@ -141,6 +142,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const showNotificationsSettings = Object.keys(enabledNotifications).length > 0;
const virtualBackgroundSupported = checkBlurSupport();
const tabs: IDialogTab<any>[] = [];
const _iAmVisitor = iAmVisitor(state);
if (showDeviceSettings) {
tabs.push({
@@ -165,7 +167,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
submit: (newState: any) => submitAudioDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
icon: IconVolumeUp
});
tabs.push({
!_iAmVisitor && tabs.push({
name: SETTINGS_TABS.VIDEO,
component: VideoDeviceSelection,
labelKey: 'settings.video',
@@ -189,7 +191,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
});
}
if (virtualBackgroundSupported) {
if (virtualBackgroundSupported && !_iAmVisitor) {
tabs.push({
name: SETTINGS_TABS.VIRTUAL_BACKGROUND,
component: VirtualBackgroundTab,
@@ -213,7 +215,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
});
}
if (showSoundsSettings || showNotificationsSettings) {
if ((showSoundsSettings || showNotificationsSettings) && !_iAmVisitor) {
tabs.push({
name: SETTINGS_TABS.NOTIFICATIONS,
component: NotificationsTab,
@@ -236,7 +238,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
});
}
if (showModeratorSettings) {
if (showModeratorSettings && !_iAmVisitor) {
tabs.push({
name: SETTINGS_TABS.MODERATOR,
component: ModeratorTab,
@@ -269,7 +271,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
});
}
if (showCalendarSettings) {
if (showCalendarSettings && !_iAmVisitor) {
tabs.push({
name: SETTINGS_TABS.CALENDAR,
component: CalendarTab,
@@ -278,7 +280,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
});
}
tabs.push({
!_iAmVisitor && tabs.push({
name: SETTINGS_TABS.SHORTCUTS,
component: ShortcutsTab,
labelKey: 'settings.shortcuts',
@@ -295,7 +297,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
icon: IconShortcuts
});
if (showMoreTab) {
if (showMoreTab && !_iAmVisitor) {
tabs.push({
name: SETTINGS_TABS.MORE,
component: MoreTab,

View File

@@ -16,6 +16,7 @@ import { isStageFilmstripEnabled } from '../filmstrip/functions';
import { isFollowMeActive } from '../follow-me/functions';
import { getParticipantsPaneConfig } from '../participants-pane/functions';
import { isReactionsEnabled } from '../reactions/functions.any';
import { iAmVisitor } from '../visitors/functions';
/**
* Used for web. Indicates if the setting section is enabled.
@@ -205,6 +206,7 @@ export function getProfileTabProps(stateful: IStateful) {
email: localParticipant?.email,
hideEmailInSettings,
hideSelfView: getHideSelfView(state),
iAmVisitor: iAmVisitor(state),
id: localParticipant?.id,
languages: LANGUAGES,
readOnlyName: isNameReadOnly(state),

View File

@@ -1,8 +1,6 @@
// @flow
import { connect } from 'react-redux';
import type { Dispatch } from 'redux';
import { IReduxState } from '../../../app/types';
import { VIDEO_SHARE_BUTTON_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
@@ -15,30 +13,25 @@ import { isSharingStatus } from '../../functions';
/**
* The type of the React {@code Component} props of {@link TileViewButton}.
*/
type Props = AbstractButtonProps & {
interface IProps extends AbstractButtonProps {
/**
* Whether or not the button is disabled.
*/
_isDisabled: boolean,
_isDisabled: boolean;
/**
* Whether or not the local participant is sharing a video.
*/
_sharingVideo: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Dispatch<any>
};
_sharingVideo: boolean;
}
/**
* Component that renders a toolbar button for toggling the tile layout view.
*
* @augments AbstractButton
*/
class VideoShareButton extends AbstractButton<Props, *> {
class VideoShareButton extends AbstractButton<IProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.sharedvideo';
icon = IconPlay;
label = 'toolbar.sharedvideo';
@@ -94,17 +87,17 @@ class VideoShareButton extends AbstractButton<Props, *> {
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {Props}
* @returns {IProps}
*/
function _mapStateToProps(state, ownProps): Object {
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { ownerId, status: sharedVideoStatus } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state).id;
const localParticipantId = getLocalParticipant(state)?.id;
const enabled = getFeatureFlag(state, VIDEO_SHARE_BUTTON_ENABLED, true);
const { visible = enabled } = ownProps;
if (ownerId !== localParticipantId) {
return {
_isDisabled: isSharingStatus(sharedVideoStatus),
_isDisabled: isSharingStatus(sharedVideoStatus ?? ''),
_sharingVideo: false,
visible
};
@@ -112,7 +105,7 @@ function _mapStateToProps(state, ownProps): Object {
return {
_isDisabled: false,
_sharingVideo: isSharingStatus(sharedVideoStatus),
_sharingVideo: isSharingStatus(sharedVideoStatus ?? ''),
visible
};
}

View File

@@ -1,22 +1,24 @@
// @flow
import React from 'react';
import { connect } from 'react-redux';
import InputDialog from '../../../base/dialog/components/native/InputDialog';
import { translate } from '../../../base/i18n/functions';
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
import AbstractSharedVideoDialog, { IProps } from '../AbstractSharedVideoDialog';
interface IState {
error: boolean;
}
/**
* Implements a component to render a display name prompt.
*/
class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
class SharedVideoDialog extends AbstractSharedVideoDialog<IState> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props) {
constructor(props: IProps) {
super(props);
this.state = {
@@ -26,15 +28,13 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
this._onSubmitValue = this._onSubmitValue.bind(this);
}
_onSubmitValue: () => boolean;
/**
* Callback to be invoked when the value of the link input is submitted.
*
* @param {string} value - The entered video link.
* @returns {boolean}
*/
_onSubmitValue(value) {
_onSubmitValue(value: string) {
const result = super._onSetVideoLink(value);
if (!result) {

View File

@@ -2,8 +2,7 @@ import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { resetSearchCriteria } from '../../actions';
import { resetSearchCriteria } from '../../actions.native';
import SpeakerStatsList from './SpeakerStatsList';
import SpeakerStatsSearch from './SpeakerStatsSearch';

View File

@@ -1,9 +1,8 @@
// @flow
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { SPEAKERSTATS_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
@@ -39,7 +38,7 @@ class SpeakerStatsButton extends AbstractSpeakerStatsButton {
* visible: boolean
* }}
*/
function _mapStateToProps(state): Object {
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, SPEAKERSTATS_ENABLED, true);
return {

View File

@@ -1,7 +1,5 @@
// @flow
import React from 'react';
import { Text, View } from 'react-native';
import { Text, View, ViewStyle } from 'react-native';
import Avatar from '../../../base/avatar/components/Avatar';
import StatelessAvatar from '../../../base/avatar/components/native/StatelessAvatar';
@@ -11,46 +9,44 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import TimeElapsed from './TimeElapsed';
import style from './styles';
type Props = {
interface IProps {
/**
* The name of the participant.
*/
displayName: string,
displayName: string;
/**
* The total milliseconds the participant has been dominant speaker.
*/
dominantSpeakerTime: number,
/**
* The id of the user.
*/
participantId: string,
dominantSpeakerTime: number;
/**
* True if the participant is no longer in the meeting.
*/
hasLeft: boolean,
hasLeft: boolean;
/**
* True if the participant is currently the dominant speaker.
*/
isDominantSpeaker: boolean
};
isDominantSpeaker: boolean;
const SpeakerStatsItem = (props: Props) =>
/**
* The id of the user.
*/
participantId: string;
}
const SpeakerStatsItem = (props: IProps) =>
(
<View
key = { props.participantId }
style = { style.speakerStatsItemContainer }>
style = { style.speakerStatsItemContainer as ViewStyle }>
<View style = { style.speakerStatsAvatar }>
{
props.hasLeft ? (
<StatelessAvatar
className = 'userAvatar'
color = { BaseTheme.palette.ui05 }
id = 'avatar'
initials = { getInitials(props.displayName) }
size = { BaseTheme.spacing[5] } />
) : (
@@ -61,7 +57,7 @@ const SpeakerStatsItem = (props: Props) =>
)
}
</View>
<View style = { style.speakerStatsNameTime } >
<View style = { style.speakerStatsNameTime as ViewStyle } >
<Text style = { [ style.speakerStatsText, props.hasLeft && style.speakerStatsLeft ] }>
{props.displayName}
</Text>

View File

@@ -1,5 +1,3 @@
// @flow
import React from 'react';
import { View } from 'react-native';
@@ -22,5 +20,4 @@ const SpeakerStatsList = () => {
);
};
export default SpeakerStatsList;

View File

@@ -6,12 +6,11 @@ import { useDispatch, useSelector } from 'react-redux';
import { IconSearch } from '../../../base/icons/svg';
import Input from '../../../base/ui/components/native/Input';
import { escapeRegexp } from '../../../base/util/helpers';
import { initSearch } from '../../actions';
import { initSearch } from '../../actions.native';
import { isSpeakerStatsSearchDisabled } from '../../functions';
import styles from './styles';
/**
* React component for display an individual user's speaker stats.
*

View File

@@ -1,6 +1,5 @@
/* @flow */
import React, { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { Text } from 'react-native';
import { translate } from '../../../base/i18n/functions';
@@ -9,23 +8,18 @@ import { createLocalizedTime } from '../timeFunctions';
/**
* The type of the React {@code Component} props of {@link TimeElapsed}.
*/
type Props = {
interface IProps extends WithTranslation {
/**
* Style for text.
*/
style: Object,
/**
* The function to translate human-readable text.
*/
t: Function,
style: Object;
/**
* The milliseconds to be converted into a human-readable format.
*/
time: number
};
time: number;
}
/**
* React component for displaying total time elapsed. Converts a total count of
@@ -34,7 +28,7 @@ type Props = {
*
* @augments Component
*/
class TimeElapsed extends PureComponent<Props> {
class TimeElapsed extends PureComponent<IProps> {
/**
* Implements React's {@link Component#render()}.
*

View File

@@ -1,12 +1,8 @@
/* eslint-disable lines-around-comment */
import React, { ReactElement } from 'react';
import { GestureResponderEvent, StyleProp } from 'react-native';
import { connect } from 'react-redux';
// @ts-ignore
import Container from '../../../base/react/components/native/Container';
// @ts-ignore
import Text from '../../../base/react/components/native/Text';
import {
AbstractCaptions,
@@ -14,10 +10,8 @@ import {
_abstractMapStateToProps
} from '../AbstractCaptions';
// @ts-ignore
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link Captions}.
*/

View File

@@ -1,5 +1,3 @@
/* eslint-disable lines-around-comment */
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
@@ -8,17 +6,13 @@ import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconSubtitles } from '../../../base/icons/svg';
import { navigate }
// @ts-ignore
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
// @ts-ignore
import { screen } from '../../../mobile/navigation/routes';
import {
AbstractClosedCaptionButton,
IAbstractProps,
_abstractMapStateToProps
} from '../AbstractClosedCaptionButton';
/**
* A button which starts/stops the transcriptions.
*/
@@ -52,7 +46,7 @@ class ClosedCaptionButton
* @private
* @returns {Props}
*/
export function mapStateToProps(state: IReduxState, ownProps: IAbstractProps) {
export function mapStateToProps(state: IReduxState, ownProps: any) {
const enabled = getFeatureFlag(state, CLOSE_CAPTIONS_ENABLED, true);
const abstractProps = _abstractMapStateToProps(state, ownProps);

View File

@@ -1,10 +1,7 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { ScrollView } from 'react-native';
import LanguageListItem from './LanguageListItem';
// @ts-ignore
import styles from './styles';
interface ILanguageListProps {
@@ -13,7 +10,6 @@ interface ILanguageListProps {
selectedLanguage: string;
}
interface ILanguageItem {
id: string;
lang: string;
@@ -30,7 +26,6 @@ const LanguageList = ({ items, onLanguageSelected }: ILanguageListProps) => {
const listItems = items?.map(item => (
<LanguageListItem
key = { item.id }
// @ts-ignore
lang = { item.lang }
onLanguageSelected = { onLanguageSelected }
selected = { item.selected } />

View File

@@ -1,5 +1,3 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { WithTranslation } from 'react-i18next';
import { StyleProp, TouchableHighlight, View, ViewStyle } from 'react-native';
@@ -9,7 +7,6 @@ import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconCheck } from '../../../base/icons/svg';
// @ts-ignore
import styles from './styles';

View File

@@ -1,21 +1,15 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
// @ts-ignore
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { goBack }
// @ts-ignore
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import AbstractLanguageSelectorDialog, {
IAbstractLanguageSelectorDialogProps
} from '../AbstractLanguageSelectorDialog';
import LanguageList from './LanguageList';
// @ts-ignore
import styles from './styles';
const LanguageSelectorDialog = (props: IAbstractLanguageSelectorDialogProps) => {
const { language, listItems, onLanguageSelected, subtitles } = props;
@@ -25,7 +19,6 @@ const LanguageSelectorDialog = (props: IAbstractLanguageSelectorDialogProps) =>
}, [ language ]);
return (
// @ts-ignore
<JitsiScreen
disableForcedKeyboardDismiss = { true }
style = { styles.subtitlesContainer }>

View File

@@ -1,7 +1,6 @@
// @flow
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { setAudioOnly, toggleAudioOnly } from '../../../base/audio-only/actions';
import { AUDIO_ONLY_BUTTON_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
@@ -16,28 +15,23 @@ import { screen } from '../../../mobile/navigation/routes';
/**
* The type of the React {@code Component} props of {@link AudioOnlyButton}.
*/
type Props = AbstractButtonProps & {
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean,
_audioOnly: boolean;
/**
* Indicates whether the car mode is enabled.
*/
_startCarMode: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
_startCarMode?: boolean;
}
/**
* An implementation of a button for toggling the audio-only mode.
*/
class AudioOnlyButton extends AbstractButton<Props, *> {
class AudioOnlyButton extends AbstractButton<IProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.audioOnly';
icon = IconAudioOnly;
label = 'toolbar.audioOnlyOn';
@@ -86,7 +80,7 @@ class AudioOnlyButton extends AbstractButton<Props, *> {
* _audioOnly: boolean
* }}
*/
function _mapStateToProps(state, ownProps): Object {
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const enabledInFeatureFlags = getFeatureFlag(state, AUDIO_ONLY_BUTTON_ENABLED, true);
const { startCarMode } = state['features/base/settings'];

View File

@@ -1,17 +1,14 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { createBreakoutRoomsEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
// @ts-ignore
import { appNavigate } from '../../../app/actions';
import { IReduxState } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { endConference } from '../../../base/conference/actions';
import { hideSheet } from '../../../base/dialog/actions';
// @ts-ignore
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
import { getLocalParticipant } from '../../../base/participants/functions';

View File

@@ -8,7 +8,6 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import HangupMenu from './HangupMenu';
/**
* Button for showing the hangup menu.
*

View File

@@ -1,9 +1,8 @@
// @flow
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCloudUpload } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
@@ -15,7 +14,7 @@ import { isSalesforceEnabled } from '../../../salesforce/functions';
/**
* Implementation of a button for opening the Salesforce link dialog.
*/
class LinkToSalesforceButton extends AbstractButton<AbstractButtonProps, *> {
class LinkToSalesforceButton extends AbstractButton<AbstractButtonProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce';
icon = IconCloudUpload;
label = 'toolbar.linkToSalesforce';
@@ -40,7 +39,7 @@ class LinkToSalesforceButton extends AbstractButton<AbstractButtonProps, *> {
* @private
* @returns {Props}
*/
function mapStateToProps(state) {
function mapStateToProps(state: IReduxState) {
return {
visible: isSalesforceEnabled(state)
};

View File

@@ -1,4 +1,3 @@
/* eslint-disable lines-around-comment */
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
@@ -8,9 +7,7 @@ import { translate } from '../../../base/i18n/functions';
import { IconCar } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { navigate }
// @ts-ignore
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
// @ts-ignore
import { screen } from '../../../mobile/navigation/routes';
/* eslint-enable lines-around-comment */
@@ -50,5 +47,4 @@ function _mapStateToProps(state: IReduxState, ownProps: AbstractButtonProps): Ob
};
}
// @ts-ignore
export default translate(connect(_mapStateToProps)(OpenCarmodeButton));

View File

@@ -1,9 +1,9 @@
// @flow
import React, { PureComponent } from 'react';
import { ViewStyle } from 'react-native';
import { Divider } from 'react-native-paper';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
@@ -32,58 +32,58 @@ import ScreenSharingButton from './ScreenSharingButton';
/**
* The type of the React {@code Component} props of {@link OverflowMenu}.
*/
type Props = {
interface IProps {
/**
* True if the overflow menu is currently visible, false otherwise.
*/
_isOpen: boolean,
/**
* Whether the recoding button should be enabled or not.
*/
_recordingEnabled: boolean,
/**
* The width of the screen.
*/
_width: number,
/**
* Whether or not the reactions feature is enabled.
*/
_reactionsEnabled: boolean,
/**
* Used for hiding the dialog when the selection was completed.
*/
dispatch: Function,
_isOpen: boolean;
/**
* Whether or not speaker stats is disable.
*/
_isSpeakerStatsDisabled: boolean
};
type State = {
_isSpeakerStatsDisabled?: boolean;
/**
* True if the bottom scheet is scrolled to the top.
* Whether or not the reactions feature is enabled.
*/
scrolledToTop: boolean
_reactionsEnabled: boolean;
/**
* Whether the recoding button should be enabled or not.
*/
_recordingEnabled: boolean;
/**
* The width of the screen.
*/
_width: number;
/**
* Used for hiding the dialog when the selection was completed.
*/
dispatch: IStore['dispatch'];
}
interface IState {
/**
* True if the bottom sheet is scrolled to the top.
*/
scrolledToTop: boolean;
}
/**
* Implements a React {@code Component} with some extra actions in addition to
* those in the toolbar.
*/
class OverflowMenu extends PureComponent<Props, State> {
class OverflowMenu extends PureComponent<IProps, IState> {
/**
* Initializes a new {@code OverflowMenu} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this.state = {
@@ -105,7 +105,8 @@ class OverflowMenu extends PureComponent<Props, State> {
const {
_isSpeakerStatsDisabled,
_reactionsEnabled,
_width
_width,
dispatch
} = this.props;
const toolbarButtons = getMovableButtons(_width);
@@ -117,6 +118,7 @@ class OverflowMenu extends PureComponent<Props, State> {
const topButtonProps = {
afterClick: this._onCancel,
dispatch,
showLabel: true,
styles: {
...bottomSheetStyles.buttons,
@@ -132,21 +134,24 @@ class OverflowMenu extends PureComponent<Props, State> {
<BottomSheet
renderFooter = { _reactionsEnabled && !toolbarButtons.has('raisehand')
? this._renderReactionMenu
: null }>
: undefined }>
<OpenCarmodeButton { ...topButtonProps } />
<AudioOnlyButton { ...buttonProps } />
{!_reactionsEnabled && !toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />}
<Divider style = { styles.divider } />
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<SecurityDialogButton { ...buttonProps } />
<RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } />
<LinkToSalesforceButton { ...buttonProps } />
<Divider style = { styles.divider } />
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<SharedVideoButton { ...buttonProps } />
{!toolbarButtons.has('screensharing') && <ScreenSharingButton { ...buttonProps } />}
{!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
<Divider style = { styles.divider } />
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<ClosedCaptionButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />
<SettingsButton { ...buttonProps } />
@@ -165,7 +170,7 @@ class OverflowMenu extends PureComponent<Props, State> {
}
/**
* Functoin to render the reaction menu as the footer of the bottom sheet.
* Function to render the reaction menu as the footer of the bottom sheet.
*
* @returns {React$Element}
*/
@@ -181,9 +186,9 @@ class OverflowMenu extends PureComponent<Props, State> {
*
* @param {Object} state - Redux state.
* @private
* @returns {Props}
* @returns {IProps}
*/
function _mapStateToProps(state) {
function _mapStateToProps(state: IReduxState) {
return {
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
_reactionsEnabled: isReactionsEnabled(state),

View File

@@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { openSheet } from '../../../base/dialog/actions';
import { OVERFLOW_MENU_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
@@ -9,21 +10,10 @@ import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/too
import OverflowMenu from './OverflowMenu';
/**
* The type of the React {@code Component} props of {@link OverflowMenuButton}.
*/
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
* An implementation of a button for showing the {@code OverflowMenu}.
*/
class OverflowMenuButton extends AbstractButton<Props, *> {
class OverflowMenuButton extends AbstractButton<AbstractButtonProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.moreActions';
icon = IconDotsHorizontal;
label = 'toolbar.moreActions';
@@ -47,7 +37,7 @@ class OverflowMenuButton extends AbstractButton<Props, *> {
* @private
* @returns {Props}
*/
function _mapStateToProps(state): Object {
function _mapStateToProps(state: IReduxState) {
const enabledFlag = getFeatureFlag(state, OVERFLOW_MENU_ENABLED, true);
return {

View File

@@ -1,10 +1,8 @@
// @flow
import { connect } from 'react-redux';
import { type Dispatch } from 'redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { RAISE_HAND_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
@@ -14,33 +12,29 @@ import {
getLocalParticipant,
hasRaisedHand
} from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link RaiseHandButton}.
*/
type Props = AbstractButtonProps & {
interface IProps extends AbstractButtonProps {
/**
* The local participant.
*/
_localParticipant: Object,
_localParticipant?: ILocalParticipant;
/**
* Whether the participant raused their hand or not.
* Whether the participant raised their hand or not.
*/
_raisedHand: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Dispatch<any>
};
_raisedHand: boolean;
}
/**
* An implementation of a button to raise or lower hand.
*/
class RaiseHandButton extends AbstractButton<Props, *> {
class RaiseHandButton extends AbstractButton<IProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
icon = IconRaiseHand;
label = 'toolbar.raiseYourHand';
@@ -88,9 +82,9 @@ class RaiseHandButton extends AbstractButton<Props, *> {
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {Props}
* @returns {IProps}
*/
function _mapStateToProps(state, ownProps): Object {
function _mapStateToProps(state: IReduxState, ownProps: any) {
const _localParticipant = getLocalParticipant(state);
const enabled = getFeatureFlag(state, RAISE_HAND_ENABLED, true);
const { visible = enabled } = ownProps;

View File

@@ -1,7 +1,6 @@
// @flow
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCameraRefresh } from '../../../base/icons/svg';
import { toggleCameraFacingMode } from '../../../base/media/actions';
@@ -12,28 +11,23 @@ import { isLocalTrackMuted } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ToggleCameraButton}.
*/
type Props = AbstractButtonProps & {
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean,
_audioOnly: boolean;
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
_videoMuted: boolean;
}
/**
* An implementation of a button for toggling the camera facing mode.
*/
class ToggleCameraButton extends AbstractButton<Props, *> {
class ToggleCameraButton extends AbstractButton<IProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera';
icon = IconCameraRefresh;
label = 'toolbar.toggleCamera';
@@ -72,7 +66,7 @@ class ToggleCameraButton extends AbstractButton<Props, *> {
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state): Object {
function _mapStateToProps(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks'];

View File

@@ -1,7 +1,6 @@
// @flow
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconAudioOnlyOff } from '../../../base/icons/svg';
import { updateSettings } from '../../../base/settings/actions';
@@ -10,23 +9,18 @@ import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/too
/**
* The type of the React {@code Component} props of {@link ToggleSelfViewButton}.
*/
type Props = AbstractButtonProps & {
interface IProps extends AbstractButtonProps {
/**
* Whether the self view is disabled or not.
*/
_disableSelfView: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
_disableSelfView: boolean;
}
/**
* An implementation of a button for toggling the self view.
*/
class ToggleSelfViewButton extends AbstractButton<Props, *> {
class ToggleSelfViewButton extends AbstractButton<IProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.selfView';
icon = IconAudioOnlyOff;
label = 'videothumbnail.hideSelfView';
@@ -69,7 +63,7 @@ class ToggleSelfViewButton extends AbstractButton<Props, *> {
* _disableSelfView: boolean
* }}
*/
function _mapStateToProps(state): Object {
function _mapStateToProps(state: IReduxState) {
const { disableSelfView } = state['features/base/settings'];
return {

View File

@@ -1,13 +1,11 @@
// @flow
import React from 'react';
import { View } from 'react-native';
import { View, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import Platform from '../../../base/react/Platform.native';
import { StyleType } from '../../../base/styles/functions.native';
import ChatButton from '../../../chat/components/native/ChatButton';
import ReactionsMenuButton from '../../../reactions/components/native/ReactionsMenuButton';
import { isReactionsEnabled } from '../../../reactions/functions.any';
@@ -27,38 +25,38 @@ import styles from './styles';
/**
* The type of {@link Toolbox}'s React {@code Component} props.
*/
type Props = {
interface IProps {
/**
* Whether the end conference feature is supported.
*/
_endConferenceSupported: boolean,
/**
* Whether or not the reactions feature is enabled.
*/
_reactionsEnabled: boolean,
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType,
/**
* The indicator which determines whether the toolbox is visible.
*/
_visible: boolean,
_endConferenceSupported: boolean;
/**
* Whether we are in visitors mode.
*/
_iAmVisitor: boolean,
_iAmVisitor: boolean;
/**
* Whether or not the reactions feature is enabled.
*/
_reactionsEnabled: boolean;
/**
* The color-schemed stylesheet of the feature.
*/
_styles: any;
/**
* The indicator which determines whether the toolbox is visible.
*/
_visible: boolean;
/**
* The width of the screen.
*/
_width: number
};
_width: number;
}
/**
* Implements the conference Toolbox on React Native.
@@ -66,7 +64,7 @@ type Props = {
* @param {Object} props - The props of the component.
* @returns {React$Element}.
*/
function Toolbox(props: Props) {
function Toolbox(props: IProps) {
const { _endConferenceSupported, _reactionsEnabled, _styles, _visible, _iAmVisitor, _width } = props;
if (!_visible) {
@@ -74,7 +72,7 @@ function Toolbox(props: Props) {
}
const bottomEdge = Platform.OS === 'ios' && _visible;
const { buttonStylesBorderless, hangupButtonStyles, hangupMenuButtonStyles, toggledButtonStyles } = _styles;
const { buttonStylesBorderless, hangupButtonStyles, toggledButtonStyles } = _styles;
const additionalButtons = getMovableButtons(_width);
const backgroundToggledStyle = {
...toggledButtonStyles,
@@ -93,12 +91,14 @@ function Toolbox(props: Props) {
return (
<View
style = { styles.toolboxContainer }>
style = { styles.toolboxContainer as ViewStyle }>
<SafeAreaView
accessibilityRole = 'toolbar'
// @ts-ignore
edges = { [ bottomEdge && 'bottom' ].filter(Boolean) }
pointerEvents = 'box-none'
style = { style }>
style = { style as ViewStyle }>
{!_iAmVisitor && <AudioMuteButton
styles = { buttonStylesBorderless }
toggledStyles = { toggledButtonStyles } />
@@ -108,9 +108,9 @@ function Toolbox(props: Props) {
toggledStyles = { toggledButtonStyles } />
}
{additionalButtons.has('chat')
&& <ChatButton
styles = { buttonStylesBorderless }
toggledStyles = { backgroundToggledStyle } />
&& <ChatButton
styles = { buttonStylesBorderless }
toggledStyles = { backgroundToggledStyle } />
}
{!_iAmVisitor && additionalButtons.has('screensharing')
&& <ScreenSharingButton styles = { buttonStylesBorderless } />}
@@ -127,9 +127,7 @@ function Toolbox(props: Props) {
toggledStyles = { toggledButtonStyles } />
}
{ _endConferenceSupported
? <HangupMenuButton
styles = { hangupMenuButtonStyles }
toggledStyles = { toggledButtonStyles } />
? <HangupMenuButton />
: <HangupButton
styles = { hangupButtonStyles } />
}
@@ -145,9 +143,9 @@ function Toolbox(props: Props) {
* @param {Object} state - The redux state of which parts are to be mapped to
* {@code Toolbox} props.
* @private
* @returns {Props}
* @returns {IProps}
*/
function _mapStateToProps(state: Object): Object {
function _mapStateToProps(state: IReduxState) {
const { conference } = state['features/base/conference'];
const endConferenceSupported = conference?.isEndConferenceSupported();

View File

@@ -2,6 +2,7 @@ import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
import ReactFocusLock from 'react-focus-lock';
import { makeStyles } from 'tss-react/mui';
import { isElementInTheViewport } from '../../../base/ui/functions.web';
import { DRAWER_MAX_HEIGHT } from '../../constants';
@@ -107,7 +108,16 @@ function Drawer({
'aria-modal': true,
'aria-labelledby': `#${headingId}`
}}
returnFocus = { true }>
returnFocus = {
// If we return the focus to an element outside the viewport the page will scroll to
// this element which in our case is undesirable and the element is outside of the
// viewport on purpose (to be hidden). For example if we return the focus to the toolbox
// when it is hidden the whole page will move up in order to show the toolbox. This is
// usually followed up with displaying the toolbox (because now it is on focus) but
// because of the animation the whole scenario looks like jumping large video.
isElementInTheViewport
}>
{children}
</ReactFocusLock>
</div>

View File

@@ -1,14 +1,25 @@
import React, { ReactNode, useCallback } from 'react';
import { useSelector } from 'react-redux';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import Popover from '../../../base/popover/components/Popover.web';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { setGifMenuVisibility } from '../../../gifs/actions';
import { isGifsMenuOpen } from '../../../gifs/functions.web';
import ReactionEmoji from '../../../reactions/components/web/ReactionEmoji';
import ReactionsMenu from '../../../reactions/components/web/ReactionsMenu';
import { REACTIONS_MENU_HEIGHT } from '../../../reactions/constants';
import {
GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU,
RAISE_HAND_ROW_HEIGHT,
REACTIONS_MENU_HEIGHT_DRAWER,
REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU
} from '../../../reactions/constants';
import { getReactionsQueue } from '../../../reactions/functions.any';
import { IReactionsMenuParent } from '../../../reactions/types';
import { DRAWER_MAX_HEIGHT } from '../../constants';
import { showOverflowDrawer } from '../../functions.web';
@@ -27,48 +38,85 @@ interface IProps {
ariaControls: string;
/**
* A child React Element to display within {@code Popover}.
* Information about the buttons that need to be rendered in the overflow menu.
*/
children: ReactNode;
buttons: Object[];
/**
* Whether or not the OverflowMenu popover should display.
*/
isOpen: boolean;
/**
* Esc key handler.
*/
onToolboxEscKey: (e?: React.KeyboardEvent) => void;
/**
* Callback to change the visibility of the overflow menu.
*/
onVisibilityChange: Function;
/**
* Whether or not to display the reactions in the mobile menu.
* Whether to show the raise hand in the reactions menu or not.
*/
showMobileReactions: boolean;
showRaiseHandInReactionsMenu: boolean;
/**
* Whether or not to display the reactions menu.
*/
showReactionsMenu: boolean;
}
const useStyles = makeStyles()(() => {
const useStyles = makeStyles<{ overflowDrawer: boolean; reactionsMenuHeight: number; }>()(
(_theme, { reactionsMenuHeight, overflowDrawer }) => {
return {
overflowMenuDrawer: {
overflowY: 'auto' as const,
height: `calc(${DRAWER_MAX_HEIGHT} - ${REACTIONS_MENU_HEIGHT}px - 16px)`
overflow: 'hidden',
height: `calc(${DRAWER_MAX_HEIGHT} - ${reactionsMenuHeight}px - 16px)`
},
contextMenu: {
position: 'relative' as const,
right: 'auto',
margin: 0,
marginBottom: '8px',
maxHeight: overflowDrawer ? undefined : 'calc(100vh - 100px)',
paddingBottom: overflowDrawer ? undefined : 0,
minWidth: '240px',
overflow: 'hidden'
},
content: {
position: 'relative',
maxHeight: overflowDrawer
? `calc(100% - ${reactionsMenuHeight}px - 16px)` : `calc(100vh - 100px - ${reactionsMenuHeight}px)`,
overflowY: 'auto'
},
footer: {
position: 'relative',
bottom: 0
}
};
});
const OverflowMenuButton = ({
children,
buttons,
isOpen,
onToolboxEscKey,
onVisibilityChange,
showMobileReactions
showRaiseHandInReactionsMenu,
showReactionsMenu
}: IProps) => {
const { classes } = useStyles();
const overflowDrawer = useSelector(showOverflowDrawer);
const reactionsQueue = useSelector(getReactionsQueue);
const isGiphyVisible = useSelector(isGifsMenuOpen);
const dispatch = useDispatch();
const onCloseDialog = useCallback(() => {
onVisibilityChange(false);
}, [ onVisibilityChange ]);
if (isGiphyVisible && !overflowDrawer) {
dispatch(setGifMenuVisibility(false));
}
}, [ onVisibilityChange, setGifMenuVisibility, isGiphyVisible, overflowDrawer, dispatch ]);
const onOpenDialog = useCallback(() => {
onVisibilityChange(true);
@@ -88,50 +136,118 @@ const OverflowMenuButton = ({
onVisibilityChange(!isOpen);
}, [ isOpen, onVisibilityChange ]);
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const { t } = useTranslation();
let reactionsMenuHeight = 0;
if (showReactionsMenu) {
reactionsMenuHeight = REACTIONS_MENU_HEIGHT_DRAWER;
if (!overflowDrawer) {
reactionsMenuHeight = REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU;
}
if (!showRaiseHandInReactionsMenu) {
reactionsMenuHeight -= RAISE_HAND_ROW_HEIGHT;
}
if (!overflowDrawer && isGiphyVisible) {
reactionsMenuHeight += GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU;
}
}
const { classes } = useStyles({
reactionsMenuHeight,
overflowDrawer
});
const groupsJSX = buttons.map((buttonGroup: any) => (
<ContextMenuItemGroup key = { `group-${buttonGroup[0].group}` }>
{buttonGroup.map(({ key, Content, ...rest }: { Content: React.ElementType; key: string; }) => {
const props: { buttonKey?: string; contextMenu?: boolean; showLabel?: boolean; } = { ...rest };
if (key !== 'reactions') {
props.buttonKey = key;
props.contextMenu = true;
props.showLabel = true;
}
return (
<Content
{ ...props }
key = { key } />);
})}
</ContextMenuItemGroup>));
const overflowMenu = groupsJSX && (
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
className = { classes.contextMenu }
hidden = { false }
id = 'overflow-context-menu'
inDrawer = { overflowDrawer }
onKeyDown = { onToolboxEscKey }>
<div className = { classes.content }>
{ groupsJSX }
</div>
{
showReactionsMenu && (<div className = { classes.footer }>
<ReactionsMenu
parent = {
overflowDrawer ? IReactionsMenuParent.OverflowDrawer : IReactionsMenuParent.OverflowMenu }
showRaisedHand = { showRaiseHandInReactionsMenu } />
</div>)
}
</ContextMenu>);
if (overflowDrawer) {
return (
<div className = 'toolbox-button-wth-dialog context-menu'>
<>
<OverflowToggleButton
handleClick = { toggleDialogVisibility }
isOpen = { isOpen }
onKeyDown = { onEscClick } />
<JitsiPortal>
<Drawer
isOpen = { isOpen }
onClose = { onCloseDialog }>
<>
<div className = { classes.overflowMenuDrawer }>
{ overflowMenu }
</div>
</>
</Drawer>
{showReactionsMenu && <div className = 'reactions-animations-container'>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>}
</JitsiPortal>
</>
</div>
);
}
return (
<div className = 'toolbox-button-wth-dialog context-menu'>
{
overflowDrawer ? (
<>
<OverflowToggleButton
handleClick = { toggleDialogVisibility }
isOpen = { isOpen }
onKeyDown = { onEscClick } />
<JitsiPortal>
<Drawer
isOpen = { isOpen }
onClose = { onCloseDialog }>
<>
<div className = { classes.overflowMenuDrawer }>
{children}
</div>
{showMobileReactions && <ReactionsMenu overflowMenu = { true } />}
</>
</Drawer>
{showMobileReactions && <div className = 'reactions-animations-container'>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>}
</JitsiPortal>
</>
) : (
<Popover
content = { children }
headingId = 'overflow-context-menu'
onPopoverClose = { onCloseDialog }
onPopoverOpen = { onOpenDialog }
position = 'top'
trigger = 'click'
visible = { isOpen }>
<OverflowToggleButton
isOpen = { isOpen }
onKeyDown = { onEscClick } />
</Popover>
)
}
<Popover
content = { overflowMenu }
headingId = 'overflow-context-menu'
onPopoverClose = { onCloseDialog }
onPopoverOpen = { onOpenDialog }
position = 'top'
trigger = 'click'
visible = { isOpen }>
<OverflowToggleButton
isOpen = { isOpen }
onKeyDown = { onEscClick } />
</Popover>
{showReactionsMenu && <div className = 'reactions-animations-container'>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>}
</div>
);
};

View File

@@ -30,7 +30,6 @@ import {
import { getLocalVideoTrack } from '../../../base/tracks/functions.web';
import { ITrack } from '../../../base/tracks/types';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { toggleChat } from '../../../chat/actions.web';
import ChatButton from '../../../chat/components/web/ChatButton';
import EmbedMeetingButton from '../../../embed-meeting/components/EmbedMeetingButton';
@@ -54,9 +53,10 @@ import {
addReactionToBuffer,
toggleReactionsMenuVisibility
} from '../../../reactions/actions.web';
import RaiseHandButton from '../../../reactions/components/web/RaiseHandButton';
import ReactionsMenuButton from '../../../reactions/components/web/ReactionsMenuButton';
import { REACTIONS } from '../../../reactions/constants';
import { isReactionsEnabled } from '../../../reactions/functions.web';
import { isReactionsButtonEnabled, isReactionsEnabled } from '../../../reactions/functions.web';
import LiveStreamButton from '../../../recording/components/LiveStream/web/LiveStreamButton';
import RecordButton from '../../../recording/components/Recording/web/RecordButton';
import { isSalesforceEnabled } from '../../../salesforce/functions';
@@ -273,6 +273,11 @@ interface IProps extends WithTranslation {
*/
_raisedHand: boolean;
/**
* Whether or not to display reactions in separate button.
*/
_reactionsButtonEnabled: boolean;
/**
* Whether or not reactions feature is enabled.
*/
@@ -717,6 +722,7 @@ class Toolbox extends Component<IProps> {
_isNarrowLayout,
_isSpeakerStatsDisabled,
_multiStreamModeEnabled,
_reactionsButtonEnabled,
_reactionsEnabled,
_screenSharing,
_shortcutsEnabled,
@@ -755,8 +761,21 @@ class Toolbox extends Component<IProps> {
group: 2
};
const raisehand = (!_reactionsEnabled || (!_isNarrowLayout && !_isMobile)) && {
// In Narrow layout and mobile web we are using drawer for popups and that is why it is better to include
// all forms of reactions in the overflow menu. Otherwise the toolbox will be hidden and the reactions popup
// misaligned.
const showReactionsAsPartOfRaiseHand
= !_reactionsButtonEnabled && _reactionsEnabled && !_isNarrowLayout && !_isMobile;
const raisehand = {
key: 'raisehand',
Content: showReactionsAsPartOfRaiseHand ? ReactionsMenuButton : RaiseHandButton,
handleClick: this._onToolbarToggleRaiseHand,
group: 2
};
const reactions = _reactionsButtonEnabled && _reactionsEnabled && {
key: 'reactions',
Content: ReactionsMenuButton,
handleClick: this._onToolbarToggleRaiseHand,
group: 2
@@ -931,6 +950,7 @@ class Toolbox extends Component<IProps> {
desktop,
chat,
raisehand,
reactions,
participants,
invite,
tileview,
@@ -1015,9 +1035,8 @@ class Toolbox extends Component<IProps> {
this._setButtonsNotifyClickMode(buttons);
const isHangupVisible = isToolbarButtonEnabled('hangup', _toolbarButtons);
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
let { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|| THRESHOLDS[THRESHOLDS.length - 1];
let sliceIndex = order.length + 2;
const keys = Object.keys(buttons);
@@ -1028,6 +1047,11 @@ class Toolbox extends Component<IProps> {
!_jwtDisabledButons.includes(key)
&& (isToolbarButtonEnabled(key, _toolbarButtons) || isToolbarButtonEnabled(alias, _toolbarButtons))
);
const filteredKeys = filtered.map(button => button.key);
order = order.filter(key => filteredKeys.includes(buttons[key as keyof typeof buttons].key));
let sliceIndex = order.length + 2;
if (isHangupVisible) {
sliceIndex -= 1;
@@ -1412,6 +1436,7 @@ class Toolbox extends Component<IProps> {
_overflowDrawer,
_overflowMenuVisible,
_reactionsEnabled,
_reactionsButtonEnabled,
_toolbarButtons,
classes,
t
@@ -1421,6 +1446,12 @@ class Toolbox extends Component<IProps> {
const containerClassName = `toolbox-content${_isMobile || _isNarrowLayout ? ' toolbox-content-mobile' : ''}`;
const { mainMenuButtons, overflowMenuButtons } = this._getVisibleButtons();
const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand');
const showReactionsInOverflowMenu
= (_reactionsEnabled && !_reactionsButtonEnabled
&& (raiseHandInOverflowMenu || _isNarrowLayout || _isMobile))
|| overflowMenuButtons.some(({ key }) => key === 'reactions');
const showRaiseHandInReactionsMenu = showReactionsInOverflowMenu && raiseHandInOverflowMenu;
return (
<div className = { containerClassName }>
@@ -1444,47 +1475,36 @@ class Toolbox extends Component<IProps> {
{Boolean(overflowMenuButtons.length) && (
<OverflowMenuButton
ariaControls = 'overflow-menu'
isOpen = { _overflowMenuVisible }
key = 'overflow-menu'
onVisibilityChange = { this._onSetOverflowVisible }
showMobileReactions = {
_reactionsEnabled && (_isMobile || _isNarrowLayout)
}>
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
className = { classes.contextMenu }
hidden = { false }
id = 'overflow-context-menu'
inDrawer = { _overflowDrawer }
onKeyDown = { this._onEscKey }>
{overflowMenuButtons.reduce((acc, val) => {
if (acc.length) {
const prev = acc[acc.length - 1];
const group = prev[prev.length - 1].group;
buttons = { overflowMenuButtons.reduce((acc, val) => {
if (val.key === 'reactions' && showReactionsInOverflowMenu) {
return acc;
}
if (group === val.group) {
prev.push(val);
} else {
acc.push([ val ]);
}
if (val.key === 'raisehand' && showRaiseHandInReactionsMenu) {
return acc;
}
if (acc.length) {
const prev = acc[acc.length - 1];
const group = prev[prev.length - 1].group;
if (group === val.group) {
prev.push(val);
} else {
acc.push([ val ]);
}
} else {
acc.push([ val ]);
}
return acc;
}, []).map((buttonGroup: any) => (
<ContextMenuItemGroup key = { `group-${buttonGroup[0].group}` }>
{buttonGroup.map(({ key, Content, ...rest }: any) => (
key !== 'raisehand' || !_reactionsEnabled)
&& <Content
{ ...rest }
buttonKey = { key }
contextMenu = { true }
key = { key }
showLabel = { true } />)}
</ContextMenuItemGroup>))}
</ContextMenu>
</OverflowMenuButton>
return acc;
}, []) }
isOpen = { _overflowMenuVisible }
key = 'overflow-menu'
onToolboxEscKey = { this._onEscKey }
onVisibilityChange = { this._onSetOverflowVisible }
showRaiseHandInReactionsMenu = { showRaiseHandInReactionsMenu }
showReactionsMenu = { showReactionsInOverflowMenu } />
)}
{ isToolbarButtonEnabled('hangup', _toolbarButtons) && (
@@ -1554,6 +1574,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const localVideo = getLocalVideoTrack(state['features/base/tracks']);
const { clientWidth } = state['features/base/responsive-ui'];
let toolbarButtons = ownProps.toolbarButtons || getToolbarButtons(state);
const _reactionsEnabled = isReactionsEnabled(state);
if (iAmVisitor(state)) {
toolbarButtons = VISITORS_MODE_BUTTONS.filter(e => toolbarButtons.indexOf(e) > -1);
@@ -1590,7 +1611,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
_overflowDrawer: overflowDrawer,
_participantsPaneOpen: getParticipantsPaneOpen(state),
_raisedHand: hasRaisedHand(localParticipant),
_reactionsEnabled: isReactionsEnabled(state),
_reactionsButtonEnabled: isReactionsButtonEnabled(state),
_reactionsEnabled,
_screenSharing: isScreenVideoShared(state),
_shortcutsEnabled: areKeyboardShortcutsEnabled(state),
_tileViewEnabled: shouldDisplayTileView(state),

View File

@@ -2,6 +2,10 @@
* Thresholds for displaying toolbox buttons.
*/
export const THRESHOLDS = [
{
width: 565,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants', 'tileview' ]
},
{
width: 520,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants', 'tileview' ]

View File

@@ -124,6 +124,16 @@ export function showOverflowDrawer(state: IReduxState) {
return state['features/toolbox'].overflowDrawer;
}
/**
* Returns true if the overflow menu button is displayed and false otherwise.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean} - True if the overflow menu button is displayed and false otherwise.
*/
export function showOverflowMenu(state: IReduxState) {
return state['features/toolbox'].overflowMenuVisible;
}
/**
* Indicates whether the toolbox is enabled or not.
*

View File

@@ -1,11 +1,9 @@
// @flow
import { WithTranslation } from 'react-i18next';
import { translate } from '../../base/i18n/functions';
import ExpandedLabel, { Props as AbstractProps } from '../../base/label/components/native/ExpandedLabel';
import ExpandedLabel, { IProps as AbstractProps } from '../../base/label/components/native/ExpandedLabel';
type Props = AbstractProps & {
t: Function
}
type Props = AbstractProps & WithTranslation;
/**
* A react {@code Component} that implements an expanded label as tooltip-like

View File

@@ -1,12 +1,10 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n/functions';
import Label from '../../base/label/components/native/Label';
import { type Props, _mapStateToProps } from './AbstractTranscribingLabel';
import { IProps, _mapStateToProps } from './AbstractTranscribingLabel';
/**
* React {@code Component} for displaying a label when a transcriber is in the
@@ -14,7 +12,7 @@ import { type Props, _mapStateToProps } from './AbstractTranscribingLabel';
*
* @augments Component
*/
class TranscribingLabel extends Component<Props> {
class TranscribingLabel extends Component<IProps> {
/**
* Renders the platform-specific label component.

View File

@@ -1,7 +1,6 @@
// @flow
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { approveParticipant } from '../../../av-moderation/actions';
import { isSupported } from '../../../av-moderation/functions';
import { translate } from '../../../base/i18n/functions';
@@ -11,34 +10,29 @@ import { getParticipantById, isLocalParticipantModerator } from '../../../base/p
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isForceMuted } from '../../../participants-pane/functions';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
export interface IProps extends AbstractButtonProps {
/**
* Whether or not the participant is audio force muted.
*/
isAudioForceMuted: boolean,
isAudioForceMuted: boolean;
/**
* Whether or not the participant is video force muted.
*/
isVideoForceMuted: boolean,
isVideoForceMuted: boolean;
/**
* The ID of the participant object that this button is supposed to
* ask to unmute.
*/
participantID: string
};
participantID: string;
}
/**
* An abstract remote video menu button which asks the remote participant to unmute.
*/
class AskUnmuteButton extends AbstractButton<Props, *> {
class AskUnmuteButton extends AbstractButton<IProps> {
accessibilityLabel = 'participantsPane.actions.askUnmute';
icon = IconMic;
label = 'participantsPane.actions.askUnmute';
@@ -91,16 +85,16 @@ class AskUnmuteButton extends AbstractButton<Props, *> {
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - Properties of component.
* @returns {Props}
* @returns {IProps}
*/
function mapStateToProps(state, ownProps) {
function mapStateToProps(state: IReduxState, ownProps: any) {
const { participantID } = ownProps;
const participant = getParticipantById(state, participantID);
return {
isAudioForceMuted: isForceMuted(participant, MEDIA_TYPE.AUDIO, state),
isVideoForceMuted: isForceMuted(participant, MEDIA_TYPE.VIDEO, state),
visible: isLocalParticipantModerator(state) && isSupported()
visible: isLocalParticipantModerator(state) && isSupported()(state)
};
}

View File

@@ -7,28 +7,18 @@ import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/too
import ConnectionStatusComponent from './ConnectionStatusComponent';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
export interface IProps extends AbstractButtonProps {
/**
* The ID of the participant that this button is supposed to pin.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
participantID: string;
}
/**
* A remote video menu button which shows the connection statistics.
*/
class ConnectionStatusButton extends AbstractButton<Props, *> {
class ConnectionStatusButton extends AbstractButton<IProps> {
icon = IconInfoCircle;
label = 'videothumbnail.connectionInfo';

View File

@@ -1,5 +1,3 @@
// @flow
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';

View File

@@ -1,5 +1,3 @@
// @flow
import React from 'react';
import { connect } from 'react-redux';
@@ -28,8 +26,6 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
onSubmit = { this._onSubmit } />
);
}
_onSubmit: () => boolean;
}
export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));

View File

@@ -1,7 +1,6 @@
// @flow
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractKickButton from '../AbstractKickButton';
@@ -12,7 +11,7 @@ import AbstractKickButton from '../AbstractKickButton';
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
function _mapStateToProps(state: IReduxState) {
return {
visible: isLocalParticipantModerator(state)
};

View File

@@ -1,5 +1,3 @@
// @flow
import React from 'react';
import { connect } from 'react-redux';
@@ -29,8 +27,6 @@ class KickRemoteParticipantDialog extends AbstractKickRemoteParticipantDialog {
title = 'dialog.kickParticipantTitle' />
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(KickRemoteParticipantDialog));

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