Compare commits

...

24 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
d2521bc67a 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-01 11:06:44 +02:00
damencho
38a293f8f6 fix(chat): Fixes long display name that overflow the message bubble. 2023-04-28 13:31:14 -05:00
damencho
13e8f992b5 fix(chat): White square when both scrolls are visible. 2023-04-28 13:31:14 -05:00
damencho
c384d0d3a9 Revert "fix(chat) Make the name fit the chat bubble"
This reverts commit e56c7070c2.
2023-04-28 13:31:14 -05:00
Дамян Минков
2b71fa512b feat: Adds conference duration to the amplitude stat. (#13294) 2023-04-28 11:09:13 -05:00
damencho
d381ceb040 feat: Shows html in notification warning. 2023-04-27 12:42:28 -05:00
damencho
e18c428f52 fix: Hide virtual background for unsupported browsers from vide preview. 2023-04-27 12:42:12 -05:00
Gabriel Borlea
9fc32dc59b fix: multiselect for invite people (#13287)
* fix: multiselect duplicates

* set multiselect height to 200px
2023-04-27 10:14:16 -05:00
robertpin
6f45622ef1 fix(dialog) Fix close animation moves whole body 2023-04-27 10:06:17 -05:00
robertpin
e56c7070c2 fix(chat) Make the name fit the chat bubble 2023-04-27 09:40:27 -05:00
damencho
0aef7a36aa fix: Fixes unresolved function. 2023-04-27 09:25:07 -05:00
damencho
c030cf941e feat: Implements amplitude events for messages count. 2023-04-27 09:24:59 -05:00
Robert Pintilii
f6760e4ac7 fix(chat) Focus input on chat open (#13285) 2023-04-27 17:23:47 +03:00
Robert Pintilii
9060c77307 ref(TS) Convert some native components to TS (#13266) 2023-04-27 08:44:20 +03:00
Дамян Минков
a1d018eef4 feat: Disables sending localstats in visitor mode. (#13279) 2023-04-27 07:38:57 +02:00
Jaya Allamsetty
3f724d8fb7 fix(visitors) Do not add tracks in redux to conference. 2023-04-26 16:51:19 -04:00
Jaya Allamsetty
49d69a5a02 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1623.0.0+c520877a...v1624.0.0+e3a8472f
2023-04-26 13:04:01 -04:00
damencho
6db9e42876 fix: Allows jicofo entering rooms without requiring a password.
The case where the main room is locked and everyone leaves it to a breakout room and then coming back allows jicofo entering without a password.
2023-04-26 09:04:55 -05:00
Jaya Allamsetty
ad3e8f9f53 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1620.0.0+7f0012f7...v1623.0.0+c520877a
2023-04-26 09:26:34 -04:00
Mihaela Dumitru
9f39caa247 feat(external-api) support assumed bandwidth bps config and command (#13164) 2023-04-26 15:32:53 +03:00
Avram Tudor
1402a63324 ref(keyboard-shortcuts) refactor keyboard shortcuts to use redux (#13260)
* ref(keyboard-shortcuts) refactor keyboard shortcuts to use redux

fix unsynced default value between config flag and local storage

* code review

* fix build
2023-04-26 11:21:42 +03:00
robertpin
a9863e65c3 fix(dial-in) Make text selectable on Dial In page 2023-04-25 19:41:27 -05:00
Robert Pintilii
646c58f7d1 ref(TS) Convert some native components to TS (#13264) 2023-04-25 13:50:52 +03:00
Дамян Минков
a78ea7ca9c fix: Updates the option for disabling iframe to show a warning. (#13263)
* fix: Updates the option for disabling iframe to show a warning.

It will give a timeout of 5 mins for the conference, before navigating away from it.

* squash: Fix lint error.

* squash: Fix mobile build.
2023-04-24 14:59:25 -05:00
150 changed files with 1555 additions and 1334 deletions

View File

@@ -19,7 +19,7 @@ buildscript {
ext {
buildToolsVersion = "31.0.0"
compileSdkVersion = 32
minSdkVersion = 23
minSdkVersion = 24
targetSdkVersion = 32
supportLibVersion = "28.0.0"

2
app.js
View File

@@ -18,7 +18,6 @@ import './react/features/base/jitsi-local-storage/setup';
import conference from './conference';
import API from './modules/API';
import UI from './modules/UI/UI';
import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
import translation from './modules/translation/translation';
// Initialize Olm as early as possible.
@@ -38,7 +37,6 @@ window.APP = {
'index.loaded': window.indexLoadedTime
},
keyboardshortcut,
translation,
UI
};

View File

@@ -140,6 +140,7 @@ import { downloadJSON } from './react/features/base/util/downloadJSON';
import { showDesktopPicker } from './react/features/desktop-picker/actions';
import { appendSuffix } from './react/features/display-name/functions';
import { maybeOpenFeedbackDialog, submitFeedback } from './react/features/feedback/actions';
import { initKeyboardShortcuts } from './react/features/keyboard-shortcuts/actions';
import { maybeSetLobbyChatMessageListener } from './react/features/lobby/actions.any';
import { setNoiseSuppressionEnabled } from './react/features/noise-suppression/actions';
import { hideNotification, showNotification, showWarningNotification } from './react/features/notifications/actions';
@@ -2308,10 +2309,7 @@ export default {
APP.UI.initConference();
if (!config.disableShortcuts) {
APP.keyboardshortcut.init();
}
dispatch(initKeyboardShortcuts());
dispatch(conferenceJoined(room));
const jwt = APP.store.getState()['features/base/jwt'];

View File

@@ -1406,6 +1406,7 @@ var config = {
disableAGC
disableAP
disableHPF
disableLocalStats
disableNS
enableTalkWhileMuted
forceJVB121Ratio

View File

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

View File

@@ -108,6 +108,10 @@
#largeVideoContainer {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
margin: 0 !important;
}
#largeVideoWrapper {

View File

@@ -57,6 +57,10 @@
line-height: 24px;
border-collapse: collapse;
* {
user-select: text;
}
thead {
text-align: left;
}

View File

@@ -84,8 +84,12 @@ Component "conference.jitmeet.example.com" "muc"
"polls";
--"token_verification";
"muc_rate_limit";
"muc_password_whitelist";
}
admins = { "focusUser@auth.jitmeet.example.com" }
muc_password_whitelist = {
"focusUser@auth.jitmeet.example.com"
}
muc_room_locking = false
muc_room_default_public_jids = true

6
globals.d.ts vendored
View File

@@ -10,12 +10,6 @@ declare global {
API: any;
conference: any;
debugLogs: any;
keyboardshortcut: {
registerShortcut: Function;
unregisterShortcut: Function;
openDialog: Function;
enable: Function;
}
};
const interfaceConfig: any;

View File

@@ -675,6 +675,7 @@
"connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
"dataChannelClosed": "Video quality impaired",
"dataChannelClosedDescription": "The bridge channel has been disconnected and thus video quality is limited to its lowest setting.",
"disabledIframe": "Embedding is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes.",
"disconnected": "disconnected",
"displayNotifications": "Display notifications for",
"dontRemindMe": "Do not remind me",

View File

@@ -115,7 +115,11 @@ import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality/actions';
import { getJitsiMeetTransport } from '../transport';
import { API_ID, ENDPOINT_TEXT_MESSAGE_NAME } from './constants';
import {
API_ID,
ASSUMED_BANDWIDTH_BPS,
ENDPOINT_TEXT_MESSAGE_NAME
} from './constants';
const logger = Logger.getLogger(__filename);
@@ -310,6 +314,23 @@ function initCommands() {
APP.store.dispatch(sendTones(tones, duration, pause));
},
'set-assumed-bandwidth-bps': value => {
logger.debug('Set assumed bandwidth bps command received', value);
if (typeof value !== 'number' || isNaN(value)) {
logger.error('Assumed bandwidth bps must be a number.');
return;
}
const { conference } = APP.store.getState()['features/base/conference'];
if (conference) {
conference.setAssumedBandwidthBps(value < ASSUMED_BANDWIDTH_BPS
? ASSUMED_BANDWIDTH_BPS
: value);
}
},
'set-follow-me': value => {
logger.debug('Set follow me command received');

View File

@@ -15,3 +15,10 @@ export const API_ID = parseURLParams(window.location).jitsi_meet_external_api_id
* The payload name for the datachannel/endpoint text message event.
*/
export const ENDPOINT_TEXT_MESSAGE_NAME = 'endpoint-text-message';
/**
* The min value that can be set for the assumed bandwidth.
* Setting it to this value means not assuming any bandwidth,
* but rather allowing the estimations to take place.
*/
export const ASSUMED_BANDWIDTH_BPS = -1;

View File

@@ -59,6 +59,7 @@ const commands = {
sendEndpointTextMessage: 'send-endpoint-text-message',
sendParticipantToRoom: 'send-participant-to-room',
sendTones: 'send-tones',
setAssumedBandwidthBps: 'set-assumed-bandwidth-bps',
setFollowMe: 'set-follow-me',
setLargeVideoParticipant: 'set-large-video-participant',
setMediaEncryptionKey: 'set-media-encryption-key',

View File

@@ -11,11 +11,11 @@ import { setColorAlpha } from '../../react/features/base/util/helpers';
import { setDocumentUrl } from '../../react/features/etherpad/actions';
import { setFilmstripVisible } from '../../react/features/filmstrip/actions.any';
import {
joinLeaveNotificationsDisabled,
setNotificationsEnabled,
showNotification
} from '../../react/features/notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../react/features/notifications/constants';
import { joinLeaveNotificationsDisabled } from '../../react/features/notifications/functions';
import {
dockToolbox,
setToolboxEnabled,

View File

@@ -1,261 +0,0 @@
/* global APP */
import { jitsiLocalStorage } from '@jitsi/js-utils';
import Logger from '@jitsi/logger';
import {
ACTION_SHORTCUT_PRESSED as PRESSED,
ACTION_SHORTCUT_RELEASED as RELEASED,
createShortcutEvent
} from '../../react/features/analytics/AnalyticsEvents';
import { sendAnalytics } from '../../react/features/analytics/functions';
import { clickOnVideo } from '../../react/features/filmstrip/actions';
import { openSettingsDialog } from '../../react/features/settings/actions';
import { SETTINGS_TABS } from '../../react/features/settings/constants';
const logger = Logger.getLogger(__filename);
/**
* Map of shortcuts. When a shortcut is registered it enters the mapping.
* @type {Map}
*/
const _shortcuts = new Map();
/**
* Map of registered keyboard keys and translation keys describing the
* action performed by the key.
* @type {Map}
*/
const _shortcutsHelp = new Map();
/**
* The key used to save in local storage if keyboard shortcuts are enabled.
*/
const _enableShortcutsKey = 'enableShortcuts';
/**
* Prefer keyboard handling of these elements over global shortcuts.
* If a button is triggered using the Spacebar it should not trigger PTT.
* If an input element is focused and M is pressed it should not mute audio.
*/
const _elementsBlacklist = [
'input',
'textarea',
'button',
'[role=button]',
'[role=menuitem]',
'[role=radio]',
'[role=tab]',
'[role=option]',
'[role=switch]',
'[role=range]',
'[role=log]'
];
/**
* An element selector for elements that have their own keyboard handling.
*/
const _focusedElementsSelector = `:focus:is(${_elementsBlacklist.join(',')})`;
/**
* Maps keycode to character, id of popover for given function and function.
*/
const KeyboardShortcut = {
init() {
this._initGlobalShortcuts();
window.onkeyup = e => {
if (!this.getEnabled()) {
return;
}
const key = this._getKeyboardKey(e).toUpperCase();
const num = parseInt(key, 10);
if (!document.querySelector(_focusedElementsSelector)) {
if (_shortcuts.has(key)) {
_shortcuts.get(key).function(e);
} else if (!isNaN(num) && num >= 0 && num <= 9) {
APP.store.dispatch(clickOnVideo(num));
}
}
};
window.onkeydown = e => {
if (!this.getEnabled()) {
return;
}
const focusedElement = document.querySelector(_focusedElementsSelector);
if (!focusedElement) {
if (this._getKeyboardKey(e).toUpperCase() === ' ') {
if (APP.conference.isLocalAudioMuted()) {
sendAnalytics(createShortcutEvent(
'push.to.talk',
PRESSED));
logger.log('Talk shortcut pressed');
APP.conference.muteAudio(false);
}
}
} else if (this._getKeyboardKey(e).toUpperCase() === 'ESCAPE') {
// Allow to remove focus from selected elements using ESC key.
if (focusedElement && focusedElement.blur) {
focusedElement.blur();
}
}
};
},
/**
* Enables/Disables the keyboard shortcuts.
* @param {boolean} value - the new value.
*/
enable(value) {
jitsiLocalStorage.setItem(_enableShortcutsKey, value);
},
getEnabled() {
// Should be enabled if not explicitly set to false
// eslint-disable-next-line no-unneeded-ternary
return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
},
getShortcutsDescriptions() {
return _shortcutsHelp;
},
/**
* Opens the {@SettingsDialog} dialog on the Shortcuts page.
*
* @returns {void}
*/
openDialog() {
APP.store.dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false));
},
/**
* Registers a new shortcut.
*
* @param shortcutChar the shortcut character triggering the action
* @param shortcutAttr the "shortcut" html element attribute mapping an
* element to this shortcut and used to show the shortcut character on the
* element tooltip
* @param exec the function to be executed when the shortcut is pressed
* @param helpDescription the description of the shortcut that would appear
* in the help menu
* @param altKey whether or not the alt key must be pressed.
*/
registerShortcut(// eslint-disable-line max-params
shortcutChar,
shortcutAttr,
exec,
helpDescription,
altKey = false) {
_shortcuts.set(altKey ? `:${shortcutChar}` : shortcutChar, {
character: shortcutChar,
function: exec,
shortcutAttr,
altKey
});
if (helpDescription) {
this._addShortcutToHelp(altKey ? `:${shortcutChar}` : shortcutChar, helpDescription);
}
},
/**
* Unregisters a shortcut.
*
* @param shortcutChar unregisters the given shortcut, which means it will
* no longer be usable
* @param altKey whether or not shortcut is combo with alt key
*/
unregisterShortcut(shortcutChar, altKey = false) {
_shortcuts.delete(altKey ? `:${shortcutChar}` : shortcutChar);
_shortcutsHelp.delete(shortcutChar);
},
/**
* @param e a KeyboardEvent
* @returns {string} e.key or something close if not supported
*/
_getKeyboardKey(e) {
// If alt is pressed a different char can be returned so this takes
// the char from the code. It also prefixes with a colon to differentiate
// alt combo from simple keypress.
if (e.altKey) {
const key = e.code.replace('Key', '');
return `:${key}`;
}
// If e.key is a string, then it is assumed it already plainly states
// the key pressed. This may not be true in all cases, such as with Edge
// and "?", when the browser cannot properly map a key press event to a
// keyboard key. To be safe, when a key is "Unidentified" it must be
// further analyzed by jitsi to a key using e.which.
if (typeof e.key === 'string' && e.key !== 'Unidentified') {
return e.key;
}
if (e.type === 'keypress'
&& ((e.which >= 32 && e.which <= 126)
|| (e.which >= 160 && e.which <= 255))) {
return String.fromCharCode(e.which);
}
// try to fallback (0-9A-Za-z and QWERTY keyboard)
switch (e.which) {
case 27:
return 'Escape';
case 191:
return e.shiftKey ? '?' : '/';
}
if (e.shiftKey || e.type === 'keypress') {
return String.fromCharCode(e.which);
}
return String.fromCharCode(e.which).toLowerCase();
},
/**
* Adds the given shortcut to the help dialog.
*
* @param shortcutChar the shortcut character
* @param shortcutDescriptionKey the description of the shortcut
* @private
*/
_addShortcutToHelp(shortcutChar, shortcutDescriptionKey) {
_shortcutsHelp.set(shortcutChar, shortcutDescriptionKey);
},
/**
* Initialise global shortcuts.
* Global shortcuts are shortcuts for features that don't have a button or
* link associated with the action. In other words they represent actions
* triggered _only_ with a shortcut.
*/
_initGlobalShortcuts() {
this.registerShortcut('?', null, () => {
sendAnalytics(createShortcutEvent('help'));
this.openDialog();
}, 'keyboardShortcuts.toggleShortcuts');
// register SPACE shortcut in two steps to insure visibility of help
// message
this.registerShortcut(' ', null, () => {
sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
logger.log('Talk shortcut released');
APP.conference.muteAudio(true);
});
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
/**
* FIXME: Currently focus keys are directly implemented below in
* onkeyup. They should be moved to the SmallVideo instead.
*/
this._addShortcutToHelp('0', 'keyboardShortcuts.focusLocal');
this._addShortcutToHelp('1-9', 'keyboardShortcuts.focusRemote');
}
};
export default KeyboardShortcut;

45
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/v1620.0.0+7f0012f7/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1626.0.0+a41aa571/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -12739,8 +12739,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1620.0.0+7f0012f7/lib-jitsi-meet.tgz",
"integrity": "sha512-F3vOUwD/ys5dGtswR3C5IK8JPDOG+71J+nhkEDLotkni/Zws8KHzf8Ye332UuetUjCTGJP3CbRFe9yJdYERe7g==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1626.0.0+a41aa571/lib-jitsi-meet.tgz",
"integrity": "sha512-0LK5EMCvtsAEQkjkrWzyniTl8BKtshAZxI3e9hGnA/RVDuULLXre7GEWd2zCP3tCfdxclkhqt7skQpfNhCTdqg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -12758,7 +12758,7 @@
"patch-package": "6.5.1",
"promise.allsettled": "1.0.4",
"sdp-transform": "2.3.0",
"strophe.js": "1.6.0",
"strophe.js": "1.5.0",
"strophejs-plugin-disco": "0.0.2",
"strophejs-plugin-stream-management": "git+https://github.com/jitsi/strophejs-plugin-stream-management#679be5902097ed612fb5062b5549f3f32b6f5f47",
"uuid": "8.1.0",
@@ -17615,18 +17615,27 @@
}
},
"node_modules/strophe.js": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.6.0.tgz",
"integrity": "sha512-LE2B6nEJNUbF2Cl/p1tLIsXVJ9l86B/Z12HYYiO3n92VwYkhJ/5vJ+1ZMdwP9eN9GP8a3nbqfS5zE9umcK0FdA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.5.0.tgz",
"integrity": "sha512-H5tE/tZxPR5xP3jhXyQwsjnMSwQMf7vrn9r1OkufTApyGHYe8WjzhsfxtL3AFhVu7vFjXPPZBrmUOTm1ccYgOA==",
"dependencies": {
"abab": "^2.0.3",
"karma-rollup-preprocessor": "^7.0.8"
},
"optionalDependencies": {
"@xmldom/xmldom": "0.8.3",
"@xmldom/xmldom": "0.8.2",
"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",
@@ -29108,8 +29117,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1620.0.0+7f0012f7/lib-jitsi-meet.tgz",
"integrity": "sha512-F3vOUwD/ys5dGtswR3C5IK8JPDOG+71J+nhkEDLotkni/Zws8KHzf8Ye332UuetUjCTGJP3CbRFe9yJdYERe7g==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1626.0.0+a41aa571/lib-jitsi-meet.tgz",
"integrity": "sha512-0LK5EMCvtsAEQkjkrWzyniTl8BKtshAZxI3e9hGnA/RVDuULLXre7GEWd2zCP3tCfdxclkhqt7skQpfNhCTdqg==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
@@ -29125,7 +29134,7 @@
"patch-package": "6.5.1",
"promise.allsettled": "1.0.4",
"sdp-transform": "2.3.0",
"strophe.js": "1.6.0",
"strophe.js": "1.5.0",
"strophejs-plugin-disco": "0.0.2",
"strophejs-plugin-stream-management": "git+https://github.com/jitsi/strophejs-plugin-stream-management#679be5902097ed612fb5062b5549f3f32b6f5f47",
"uuid": "8.1.0",
@@ -32836,16 +32845,22 @@
"dev": true
},
"strophe.js": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.6.0.tgz",
"integrity": "sha512-LE2B6nEJNUbF2Cl/p1tLIsXVJ9l86B/Z12HYYiO3n92VwYkhJ/5vJ+1ZMdwP9eN9GP8a3nbqfS5zE9umcK0FdA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.5.0.tgz",
"integrity": "sha512-H5tE/tZxPR5xP3jhXyQwsjnMSwQMf7vrn9r1OkufTApyGHYe8WjzhsfxtL3AFhVu7vFjXPPZBrmUOTm1ccYgOA==",
"requires": {
"@xmldom/xmldom": "0.8.7",
"@xmldom/xmldom": "0.8.2",
"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",

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/v1620.0.0+7f0012f7/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1626.0.0+a41aa571/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -180,7 +180,7 @@
},
"overrides": {
"strophe.js@1.6.0": {
"@xmldom/xmldom": "0.8.7"
"@xmldom/xmldom": "0.8.7"
}
},
"engines": {

View File

@@ -129,14 +129,6 @@ export function appNavigate(uri?: string) {
dispatch(setLocationURL(locationURL));
dispatch(setConfig(config));
if (inIframe() && getState()['features/base/config'].disableIframeAPI) {
// in case iframeAPI is disabled redirect to the promotional page
dispatch(redirectToStaticPage('static/close3.html', `#jitsi_meet_external_api_id=${API_ID}`));
return;
}
dispatch(setRoom(room));
};
}

View File

@@ -5,6 +5,7 @@ import '../base/media/middleware';
import '../dynamic-branding/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';
import '../keyboard-shortcuts/middleware';
import '../no-audio-signal/middleware';
import '../notifications/middleware';
import '../noise-detection/middleware';

View File

@@ -3,6 +3,7 @@ import '../base/tooltip/reducer';
import '../e2ee/reducer';
import '../face-landmarks/reducer';
import '../feedback/reducer';
import '../keyboard-shortcuts/reducer';
import '../no-audio-signal/reducer';
import '../noise-detection/reducer';
import '../participants-pane/reducer';

View File

@@ -43,6 +43,7 @@ import { IGifsState } from '../gifs/reducer';
import { IGoogleApiState } from '../google-api/reducer';
import { IInviteState } from '../invite/reducer';
import { IJaaSState } from '../jaas/reducer';
import { IKeyboardShortcutsState } from '../keyboard-shortcuts/types';
import { ILargeVideoState } from '../large-video/reducer';
import { ILobbyState } from '../lobby/reducer';
import { IMobileAudioModeState } from '../mobile/audio-mode/reducer';
@@ -133,6 +134,7 @@ export interface IReduxState {
'features/google-api': IGoogleApiState;
'features/invite': IInviteState;
'features/jaas': IJaaSState;
'features/keyboard-shortcuts': IKeyboardShortcutsState;
'features/large-video': ILargeVideoState;
'features/lobby': ILobbyState;
'features/mobile/audio-mode': IMobileAudioModeState;

View File

@@ -3,6 +3,7 @@ import { sendAnalytics } from '../../analytics/functions';
import { appNavigate } from '../../app/actions';
import { IReduxState, IStore } from '../../app/types';
import { endpointMessageReceived } from '../../subtitles/actions.any';
import { iAmVisitor } from '../../visitors/functions';
import { getReplaceParticipant } from '../config/functions';
import { disconnect } from '../connection/actions';
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants';
@@ -450,11 +451,12 @@ export function conferenceUniqueIdSet(conference: IJitsiConference) {
*/
export function _conferenceWillJoin(conference: IJitsiConference) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const localTracks
= getLocalTracks(getState()['features/base/tracks'])
= getLocalTracks(state['features/base/tracks'])
.map(t => t.jitsiTrack);
if (localTracks.length) {
if (localTracks.length && !iAmVisitor(state)) {
_addLocalTracksToConference(conference, localTracks);
}
@@ -806,7 +808,7 @@ export function setStartReactionsMuted(muted: boolean, updateBackend = false) {
export function setPassword(
conference: IJitsiConference | undefined,
method: Function | undefined,
password: string) {
password?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (!conference) {
return;

View File

@@ -308,6 +308,7 @@ export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
muc: config.oldConfig.hosts.muc
},
focusUserJid: focusJid,
disableLocalStats: false,
bosh: config.oldConfig.bosh && appendURLParam(config.oldConfig.bosh, 'customusername', username),
websocket: config.oldConfig.websocket
&& appendURLParam(config.oldConfig.websocket, 'customusername', username),
@@ -338,6 +339,7 @@ export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
},
focusUserJid: focusJid,
disableFocus: true, // This flag disables sending the initial conference request
disableLocalStats: true,
bosh: config.bosh && appendURLParam(config.bosh, 'vnode', vnode),
websocket: config.websocket && appendURLParam(config.websocket, 'vnode', vnode)
};

View File

@@ -104,6 +104,7 @@ export interface IJitsiConference {
sendTextMessage: Function;
sendTones: Function;
sessionId: string;
setAssumedBandwidthBps: (value: number) => void;
setDesktopSharingFrameRate: Function;
setDisplayName: Function;
setLocalParticipantProperty: Function;

View File

@@ -35,7 +35,7 @@ export type DialogProps = {
/**
* The handler for the event when submitting the dialog.
*/
onSubmit: Function;
onSubmit?: Function;
/**
* Additional style to be applied on the dialog.

View File

@@ -29,7 +29,7 @@ interface IState {
* A react {@code Component} that implements an expanded label as tooltip-like
* component to explain the meaning of the {@code Label}.
*/
export default class ExpandedLabel<P extends IProps> extends Component<P, IState> {
export default abstract class ExpandedLabel<P extends IProps> extends Component<P, IState> {
/**
* Instantiates a new {@code ExpandedLabel} instance.
*
@@ -85,7 +85,7 @@ export default class ExpandedLabel<P extends IProps> extends Component<P, IState
*
* @returns {string}
*/
_getLabel: () => string;
abstract _getLabel(): string;
/**
* Defines the color of the expanded label. This function returns a default

View File

@@ -15,6 +15,8 @@ interface IProps {
* prop of the native component.
*/
size?: 'large' | 'small' | 'medium';
style?: any;
}
/**

View File

@@ -163,7 +163,7 @@ class MultiSelectAutocomplete extends Component<IProps, IState> {
const autoFocus = this.props.shouldFocus || false;
const disabled = this.props.isDisabled || false;
const placeholder = this.props.placeholder || '';
const noMatchesFound = this.props.noMatchesFound || '';
const noMatchesFound = this.state.loading ? this.props.loadingMessage : this.props.noMatchesFound || '';
const errorDialog = this._renderError();
return (
@@ -200,7 +200,7 @@ class MultiSelectAutocomplete extends Component<IProps, IState> {
error: this.state.error && Boolean(filterValue),
filterValue,
isOpen: Boolean(this.state.items.length) && Boolean(filterValue),
items: filterValue ? this.state.items : [],
items: [],
loading: Boolean(filterValue)
});
if (filterValue) {

View File

@@ -24,6 +24,8 @@ interface IProps {
selectedItems?: MultiSelectItem[];
}
const MULTI_SELECT_HEIGHT = 200;
const useStyles = makeStyles()(theme => {
return {
container: {
@@ -41,7 +43,7 @@ const useStyles = makeStyles()(theme => {
borderRadius: `${Number(theme.shape.borderRadius)}px`,
...withPixelLineHeight(theme.typography.bodyShortRegular),
zIndex: 2,
maxHeight: '400px',
maxHeight: `${MULTI_SELECT_HEIGHT}px`,
overflowY: 'auto',
padding: '0'
},
@@ -128,7 +130,7 @@ const MultiSelect = ({
</div>
</div>
))
: <div>{noMatchesText}</div>
: <div className = { classes.listItem }>{noMatchesText}</div>
}
</div>
), [ items ]);

View File

@@ -1,5 +1,6 @@
/* eslint-disable lines-around-comment, max-len */
import { IParticipant } from '../base/participants/types';
import { navigate }
// @ts-ignore
from '../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
@@ -21,7 +22,7 @@ export * from './actions.any';
* type: OPEN_CHAT
* }}
*/
export function openChat(participant: Object, disablePolls?: boolean) {
export function openChat(participant: IParticipant | undefined | Object, disablePolls?: boolean) {
if (disablePolls) {
navigate(screen.conference.chat);
}

View File

@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { Platform } from 'react-native';
import { WithTranslation } from 'react-i18next';
import { Platform, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { translate } from '../../../base/i18n/functions';
@@ -10,47 +11,42 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import styles from './styles';
type Props = {
interface IProps extends WithTranslation {
/**
* Callback to invoke on message send.
*/
onSend: Function,
onSend: Function;
}
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
type State = {
interface IState {
/**
* Boolean to show if an extra padding needs to be added to the bar.
*/
addPadding: boolean,
addPadding: boolean;
/**
* The value of the input field.
*/
message: string,
message: string;
/**
* Boolean to show or hide the send button.
*/
showSend: boolean
};
showSend: boolean;
}
/**
* Implements the chat input bar with text field and action(s).
*/
class ChatInputBar extends Component<Props, State> {
class ChatInputBar extends Component<IProps, IState> {
/**
* Instantiates a new instance of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this.state = {
@@ -76,7 +72,7 @@ class ChatInputBar extends Component<Props, State> {
style = { [
styles.inputBar,
this.state.addPadding ? styles.extraBarPadding : null
] }>
] as ViewStyle[] }>
<Input
blurOnSubmit = { false }
customStyles = {{ container: styles.customInputContainer }}
@@ -98,30 +94,26 @@ class ChatInputBar extends Component<Props, State> {
);
}
_onChangeText: string => void;
/**
* Callback to handle the change of the value of the text field.
*
* @param {string} text - The current value of the field.
* @returns {void}
*/
_onChangeText(text) {
_onChangeText(text: string) {
this.setState({
message: text,
showSend: Boolean(text)
});
}
_onFocused: boolean => Function;
/**
* Constructs a callback to be used to update the padding of the field if necessary.
*
* @param {boolean} focused - True of the field is focused.
* @returns {Function}
*/
_onFocused(focused) {
_onFocused(focused: boolean) {
return () => {
Platform.OS === 'android' && this.setState({
addPadding: focused
@@ -129,8 +121,6 @@ class ChatInputBar extends Component<Props, State> {
};
}
_onSubmit: () => void;
/**
* Callback to handle the submit event of the text field.
*

View File

@@ -1,14 +1,15 @@
import React from 'react';
import { Text, View } from 'react-native';
import { Text, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { translate } from '../../../base/i18n/functions';
import Linkify from '../../../base/react/components/native/Linkify';
import { isGifMessage } from '../../../gifs/functions';
import { isGifMessage } from '../../../gifs/functions.native';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import { replaceNonUnicodeEmojis } from '../../functions';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
import AbstractChatMessage, { IProps } from '../AbstractChatMessage';
import GifMessage from './GifMessage';
import PrivateMessageButton from './PrivateMessageButton';
@@ -18,7 +19,7 @@ import styles from './styles';
/**
* Renders a single chat message.
*/
class ChatMessage extends AbstractChatMessage<Props> {
class ChatMessage extends AbstractChatMessage<IProps> {
/**
* Implements {@code Component#render}.
*
@@ -31,18 +32,18 @@ class ChatMessage extends AbstractChatMessage<Props> {
// Style arrays that need to be updated in various scenarios, such as
// error messages or others.
const detailsWrapperStyle = [
styles.detailsWrapper
const detailsWrapperStyle: ViewStyle[] = [
styles.detailsWrapper as ViewStyle
];
const messageBubbleStyle = [
styles.messageBubble
const messageBubbleStyle: ViewStyle[] = [
styles.messageBubble as ViewStyle
];
if (localMessage) {
// This is a message sent by the local participant.
// The wrapper needs to be aligned to the right.
detailsWrapperStyle.push(styles.ownMessageDetailsWrapper);
detailsWrapperStyle.push(styles.ownMessageDetailsWrapper as ViewStyle);
// The bubble needs some additional styling
messageBubbleStyle.push(styles.localMessageBubble);
@@ -69,11 +70,11 @@ class ChatMessage extends AbstractChatMessage<Props> {
const messageText = replaceNonUnicodeEmojis(this._getMessageText());
return (
<View style = { styles.messageWrapper } >
<View style = { styles.messageWrapper as ViewStyle } >
{ this._renderAvatar() }
<View style = { detailsWrapperStyle }>
<View style = { messageBubbleStyle }>
<View style = { styles.textWrapper } >
<View style = { styles.textWrapper as ViewStyle } >
{ this._renderDisplayName() }
{ isGifMessage(messageText)
? <GifMessage message = { messageText } />
@@ -94,12 +95,6 @@ class ChatMessage extends AbstractChatMessage<Props> {
);
}
_getFormattedTimestamp: () => string;
_getMessageText: () => string;
_getPrivateNoticeMessage: () => string;
/**
* Renders the avatar of the sender.
*
@@ -171,7 +166,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
return (
<View style = { styles.replyContainer }>
<View style = { styles.replyContainer as ViewStyle }>
<PrivateMessageButton
isLobbyMessage = { lobbyChat }
participantID = { message.id }
@@ -204,9 +199,9 @@ class ChatMessage extends AbstractChatMessage<Props> {
* 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) {
function _mapStateToProps(state: IReduxState) {
return {
knocking: state['features/lobby'].knocking
};

View File

@@ -1,28 +1,29 @@
import React, { Component, ReactElement } from 'react';
import React, { Component } from 'react';
import { FlatList } from 'react-native';
import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants';
import { IMessage } from '../../reducer';
import ChatMessage from './ChatMessage';
type Props = {
interface IProps {
/**
* The messages array to render.
*/
messages: Array<Object>
/**
* The messages array to render.
*/
messages: Array<IMessage>;
}
/**
* Implements a container to render all the chat messages in a conference.
*/
export default class ChatMessageGroup extends Component<Props> {
export default class ChatMessageGroup extends Component<IProps> {
/**
* Instantiates a new instance of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this._keyExtractor = this._keyExtractor.bind(this);
@@ -44,29 +45,25 @@ export default class ChatMessageGroup extends Component<Props> {
);
}
_keyExtractor: Object => string;
/**
* Key extractor for the flatlist.
*
* @param {Object} item - The flatlist item that we need the key to be
* @param {Object} _item - The flatlist item that we need the key to be
* generated for.
* @param {number} index - The index of the element.
* @returns {string}
*/
_keyExtractor(item, index) {
_keyExtractor(_item: Object, index: number) {
return `key_${index}`;
}
_renderMessage: Object => ReactElement;
/**
* Renders a single chat message.
*
* @param {Object} message - The chat message to render.
* @returns {React$Element<*>}
*/
_renderMessage({ index, item: message }) {
_renderMessage({ index, item: message }: { index: number; item: IMessage; }) {
return (
<ChatMessage
message = { message }

View File

@@ -1,26 +1,26 @@
import React from 'react';
import { Image, View } from 'react-native';
import { Image, ImageStyle, View } from 'react-native';
import { GIF_PREFIX } from '../../../gifs/constants';
import styles from './styles';
type Props = {
interface IProps {
/**
* The formatted gif message.
*/
message: string
message: string;
}
const GifMessage = ({ message }: Props) => {
const GifMessage = ({ message }: IProps) => {
const url = message.substring(GIF_PREFIX.length, message.length - 1);
return (<View
style = { styles.gifContainer }>
<Image
source = {{ uri: url }}
style = { styles.gifImage } />
style = { styles.gifImage as ImageStyle } />
</View>);
};

View File

@@ -1,32 +1,32 @@
import React, { ReactElement } from 'react';
import { FlatList, Text, View } from 'react-native';
import React from 'react';
import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractMessageContainer, { type Props as AbstractProps }
from '../AbstractMessageContainer';
import { IMessage } from '../../reducer';
import AbstractMessageContainer, { IProps as AbstractProps } from '../AbstractMessageContainer';
import ChatMessageGroup from './ChatMessageGroup';
import styles from './styles';
type Props = AbstractProps & {
interface IProps extends AbstractProps {
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
t: Function;
}
/**
* Implements a container to render all the chat messages in a conference.
*/
class MessageContainer extends AbstractMessageContainer<Props> {
class MessageContainer extends AbstractMessageContainer<IProps, any> {
/**
* Instantiates a new instance of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this._keyExtractor = this._keyExtractor.bind(this);
@@ -57,24 +57,18 @@ class MessageContainer extends AbstractMessageContainer<Props> {
);
}
_getMessagesGroupedBySender: () => Array<Array<Object>>;
_keyExtractor: Object => string;
/**
* Key extractor for the flatlist.
*
* @param {Object} item - The flatlist item that we need the key to be
* @param {Object} _item - The flatlist item that we need the key to be
* generated for.
* @param {number} index - The index of the element.
* @returns {string}
*/
_keyExtractor(item, index) {
_keyExtractor(_item: Object, index: number) {
return `key_${index}`;
}
_renderListEmptyComponent: () => ReactElement;
/**
* Renders a message when there are no messages in the chat yet.
*
@@ -84,23 +78,21 @@ class MessageContainer extends AbstractMessageContainer<Props> {
const { t } = this.props;
return (
<View style = { styles.emptyComponentWrapper }>
<Text style = { styles.emptyComponentText }>
<View style = { styles.emptyComponentWrapper as ViewStyle }>
<Text style = { styles.emptyComponentText as TextStyle }>
{ t('chat.noMessagesMessage') }
</Text>
</View>
);
}
_renderMessageGroup: Object => ReactElement;
/**
* Renders a single chat message.
*
* @param {Array<Object>} messages - The chat message to render.
* @returns {React$Element<*>}
*/
_renderMessageGroup({ item: messages }) {
_renderMessageGroup({ item: messages }: { item: IMessage[]; }) {
return <ChatMessageGroup messages = { messages } />;
}
}

View File

@@ -1,57 +1,49 @@
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';
import { IconMessage, IconReply } from '../../../base/icons/svg';
import { getParticipantById } from '../../../base/participants/functions';
import { IParticipant } from '../../../base/participants/types';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { handleLobbyChatInitialized, openChat } from '../../../chat/actions.native';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { handleLobbyChatInitialized, openChat } from '../../actions.native';
export type Props = AbstractButtonProps & {
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* The ID of the participant that the message is to be sent.
*/
participantID: string,
/**
* True if the button is rendered as a reply button.
*/
reply: boolean,
/**
* Function to be used to translate i18n labels.
*/
t: Function,
/**
* True if the polls feature is disabled.
*/
_isPollsDisabled: boolean,
export interface IProps extends AbstractButtonProps {
/**
* True if message is a lobby chat message.
*/
_isLobbyMessage: boolean,
_isLobbyMessage: boolean;
/**
* True if the polls feature is disabled.
*/
_isPollsDisabled?: boolean;
/**
* The participant object retrieved from Redux.
*/
_participant: Object,
};
_participant?: IParticipant;
/**
* The ID of the participant that the message is to be sent.
*/
participantID: string;
/**
* True if the button is rendered as a reply button.
*/
reply: boolean;
}
/**
* Class to render a button that initiates the sending of a private message through chet.
* Class to render a button that initiates the sending of a private message through chat.
*/
class PrivateMessageButton extends AbstractButton<Props, any> {
class PrivateMessageButton extends AbstractButton<IProps, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage';
icon = IconMessage;
label = 'toolbar.privateMessage';
@@ -99,10 +91,10 @@ class PrivateMessageButton extends AbstractButton<Props, any> {
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
* @param {IProps} ownProps - The own props of the component.
* @returns {IProps}
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
export function _mapStateToProps(state: IReduxState, ownProps: any) {
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
const { disablePolls } = state['features/base/config'];
const { visible = enabled, isLobbyMessage, participantID } = ownProps;

View File

@@ -90,6 +90,8 @@ class ChatInput extends Component<IProps, IState> {
if (isMobileBrowser()) {
// Ensure textarea is not focused when opening chat on mobile browser.
this._textArea?.current && this._textArea.current.blur();
} else {
this._focus();
}
}

View File

@@ -29,9 +29,9 @@ const styles = (theme: Theme) => {
chatMessage: {
display: 'inline-flex',
padding: '12px',
marginRight: '12px',
backgroundColor: theme.palette.ui02,
borderRadius: '4px 12px 12px 12px',
boxSizing: 'border-box' as const,
maxWidth: '100%',
marginTop: '4px',

View File

@@ -0,0 +1,12 @@
import { IFRAME_EMBED_ALLOWED_LOCATIONS as ADDITIONAL_LOCATIONS } from './extraConstants';
/**
* Timeout of the conference when iframe is disabled in minutes.
*/
export const IFRAME_DISABLED_TIMEOUT_MINUTES = 5;
/**
* A list of allowed location to embed iframe.
*/
/* eslint-disable-next-line no-extra-parens*/
export const IFRAME_EMBED_ALLOWED_LOCATIONS = ([] as string[]).concat(ADDITIONAL_LOCATIONS);

View File

@@ -0,0 +1,5 @@
/**
* Deploy-specific configuration constants.
*/
export const IFRAME_EMBED_ALLOWED_LOCATIONS = [];

View File

@@ -1,8 +1,11 @@
import i18n from 'i18next';
import { batch } from 'react-redux';
// @ts-expect-error
import { API_ID } from '../../../modules/API/constants';
import { appNavigate } from '../app/actions';
import { IStore } from '../app/types';
import { redirectToStaticPage } from '../app/actions.any';
import { IReduxState, IStore } from '../app/types';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
@@ -15,17 +18,22 @@ import { getURLWithoutParamsNormalized } from '../base/connection/utils';
import { hideDialog } from '../base/dialog/actions';
import { isDialogOpen } from '../base/dialog/functions';
import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
import { translateToHTML } from '../base/i18n/functions';
import i18next from '../base/i18n/i18next';
import { browser } from '../base/lib-jitsi-meet';
import { pinParticipant } from '../base/participants/actions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { SET_REDUCED_UI } from '../base/responsive-ui/actionTypes';
import { BUTTON_TYPES } from '../base/ui/constants.any';
import { inIframe } from '../base/util/iframeUtils';
import { isCalendarEnabled } from '../calendar-sync/functions';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import FeedbackDialog from '../feedback/components/FeedbackDialog';
import { setFilmstripEnabled } from '../filmstrip/actions.any';
import { hideNotification, showNotification } from '../notifications/actions';
import { isVpaasMeeting } from '../jaas/functions';
import { hideNotification, showNotification, showWarningNotification } from '../notifications/actions';
import {
CALENDAR_NOTIFICATION_ID,
NOTIFICATION_ICON,
@@ -36,6 +44,7 @@ import { setToolboxEnabled } from '../toolbox/actions.any';
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
import { dismissCalendarNotification, notifyKickedOut } from './actions';
import { IFRAME_DISABLED_TIMEOUT_MINUTES, IFRAME_EMBED_ALLOWED_LOCATIONS } from './constants';
let intervalID: any;
@@ -155,6 +164,49 @@ function _conferenceJoined({ dispatch, getState }: IStore) {
}
dispatch(showSalesforceNotification());
_checkIframe(getState(), dispatch);
}
/**
* Additional checks for embedding in iframe.
*
* @param {IReduxState} state - The current state of the app.
* @param {Function} dispatch - The Redux dispatch function.
* @private
* @returns {void}
*/
function _checkIframe(state: IReduxState, dispatch: IStore['dispatch']) {
let allowIframe = false;
if (document.referrer === '') {
// no iframe
allowIframe = true;
} else {
try {
allowIframe = IFRAME_EMBED_ALLOWED_LOCATIONS.includes(new URL(document.referrer).hostname);
} catch (e) {
// wrong URL in referrer
}
}
if (inIframe() && state['features/base/config'].disableIframeAPI && !browser.isElectron()
&& !isVpaasMeeting(state) && !allowIframe) {
// show sticky notification and redirect in 5 minutes
dispatch(showWarningNotification({
description: translateToHTML(
i18next.t.bind(i18next),
'notify.disabledIframe',
{
timeout: IFRAME_DISABLED_TIMEOUT_MINUTES
}
)
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
setTimeout(() => {
// redirect to the promotional page
dispatch(redirectToStaticPage('static/close3.html', `#jitsi_meet_external_api_id=${API_ID}`));
}, IFRAME_DISABLED_TIMEOUT_MINUTES * 60 * 1000);
}
}
/**

View File

@@ -15,6 +15,7 @@ import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
import { getHideSelfView } from '../../../base/settings/functions.any';
import { registerShortcut, unregisterShortcut } from '../../../keyboard-shortcuts/actions';
import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS } from '../../../video-layout/constants';
@@ -295,12 +296,12 @@ class Filmstrip extends PureComponent <IProps, IState> {
* @inheritdoc
*/
componentDidMount() {
APP.keyboardshortcut.registerShortcut(
'F',
'filmstripPopover',
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
this.props.dispatch(registerShortcut({
character: 'F',
helpDescription: 'keyboardShortcuts.toggleFilmstrip',
handler: this._onShortcutToggleFilmstrip
}));
document.addEventListener('mouseup', this._onDragMouseUp);
// @ts-ignore
@@ -313,7 +314,8 @@ class Filmstrip extends PureComponent <IProps, IState> {
* @inheritdoc
*/
componentWillUnmount() {
APP.keyboardshortcut.unregisterShortcut('F');
this.props.dispatch(unregisterShortcut('F'));
document.removeEventListener('mouseup', this._onDragMouseUp);
// @ts-ignore

View File

@@ -68,7 +68,7 @@ export interface IProps {
/**
* Whether or not to allow sip invites.
*/
_sipInviteEnabled: boolean;
_sipInviteEnabled: boolean;
/**
* The Redux dispatch function.
@@ -92,7 +92,7 @@ export interface IState {
/**
* The list of invite items.
*/
inviteItems: Array<IInviteSelectItem>;
inviteItems: Array<IInvitee | IInviteSelectItem>;
}
/**

View File

@@ -1,7 +1,6 @@
/* eslint-disable lines-around-comment */
import _ from 'lodash';
import React, { ReactElement } from 'react';
import { WithTranslation } from 'react-i18next';
import {
ActivityIndicator,
FlatList,
@@ -23,7 +22,6 @@ import {
IconSearch,
IconShare
} from '../../../../base/icons/svg';
// @ts-ignore
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
import AvatarListItem from '../../../../base/react/components/native/AvatarListItem';
import { Item } from '../../../../base/react/types';
@@ -33,19 +31,16 @@ import HeaderNavigationButton
from '../../../../mobile/navigation/components/HeaderNavigationButton';
import { beginShareRoom } from '../../../../share-room/actions';
import { INVITE_TYPES } from '../../../constants';
import { IInviteSelectItem, IInvitee } from '../../../types';
import AbstractAddPeopleDialog, {
// @ts-ignore
type Props as AbstractProps,
// @ts-ignore
type State as AbstractState,
type IProps as AbstractProps,
type IState as AbstractState,
_mapStateToProps as _abstractMapStateToProps
} from '../AbstractAddPeopleDialog';
// @ts-ignore
import styles, { AVATAR_SIZE } from './styles';
interface IProps extends AbstractProps {
interface IProps extends AbstractProps, WithTranslation {
/**
* True if the invite dialog should be open, false otherwise.
@@ -55,12 +50,7 @@ interface IProps extends AbstractProps {
/**
* Default prop for navigation between screen components(React Navigation).
*/
navigation: Object;
/**
* Function used to translate i18n labels.
*/
t: Function;
navigation: any;
/**
* Theme used for styles.
@@ -95,9 +85,7 @@ interface IState extends AbstractState {
/**
* Implements a special dialog to invite people from a directory service.
*/
class AddPeopleDialog
// @ts-ignore
extends AbstractAddPeopleDialog<IProps, IState> {
class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
/**
* Default state object to reset the state to when needed.
*/
@@ -119,7 +107,7 @@ class AddPeopleDialog
searchTimeout: number;
/**
* Contrustor of the component.
* Constructor of the component.
*
* @inheritdoc
*/
@@ -151,7 +139,6 @@ class AddPeopleDialog
componentDidMount() {
const { navigation, t } = this.props;
// @ts-ignore
navigation.setOptions({
headerRight: () => (
<HeaderNavigationButton
@@ -171,7 +158,6 @@ class AddPeopleDialog
componentDidUpdate(prevProps: IProps) {
const { navigation, t } = this.props;
// @ts-ignore
navigation.setOptions({
// eslint-disable-next-line react/no-multi-comp
headerRight: () => (
@@ -197,12 +183,9 @@ class AddPeopleDialog
*/
render() {
const {
// @ts-ignore
_addPeopleEnabled,
// @ts-ignore
_dialOutEnabled
} = this.props;
// @ts-ignore
const { inviteItems, selectableItems } = this.state;
let placeholderKey = 'searchPlaceholder';
@@ -302,7 +285,6 @@ class AddPeopleDialog
});
// Clear search results
// @ts-ignore
this._onTypeQuery('');
}
@@ -314,10 +296,9 @@ class AddPeopleDialog
_onInvite() {
// @ts-ignore
this._invite(this.state.inviteItems)
.then((invitesLeftToSend: ArrayLike<any>) => {
.then((invitesLeftToSend: IInvitee[]) => {
if (invitesLeftToSend.length) {
this.setState({
// @ts-ignore
inviteItems: invitesLeftToSend
});
this._showFailedInviteAlert();
@@ -333,26 +314,22 @@ class AddPeopleDialog
*/
_onPressItem(item: Item) {
return () => {
// @ts-ignore
const { inviteItems } = this.state;
const finderKey = item.type === INVITE_TYPES.PHONE ? 'number' : 'user_id';
if (inviteItems.find(
// @ts-ignore
_.matchesProperty(finderKey, item[finderKey]))) {
_.matchesProperty(finderKey, item[finderKey as keyof typeof item]))) {
// Item is already selected, need to unselect it.
this.setState({
// @ts-ignore
inviteItems: inviteItems.filter(
// @ts-ignore
(element: any) => item[finderKey] !== element[finderKey])
(element: any) => item[finderKey as keyof typeof item] !== element[finderKey])
});
} else {
// Item is not selected yet, need to add to the list.
const items: Array<Object> = inviteItems.concat(item);
// @ts-ignore
const items = inviteItems.concat(item);
this.setState({
// @ts-ignore
inviteItems: _.sortBy(items, [ 'name', 'number' ])
});
}
@@ -365,12 +342,10 @@ class AddPeopleDialog
* @returns {void}
*/
_onShareMeeting() {
// @ts-ignore
if (this.state.inviteItems.length > 0) {
// The use probably intended to invite people.
this._onInvite();
} else {
// @ts-ignore
this.props.dispatch(beginShareRoom());
}
}
@@ -388,7 +363,6 @@ class AddPeopleDialog
});
clearTimeout(this.searchTimeout);
// @ts-ignore
this.searchTimeout = setTimeout(() => {
this.setState({
searchInprogress: true
@@ -462,9 +436,8 @@ class AddPeopleDialog
*/
_renderItem(flatListItem: any, index: number): ReactElement | null {
const { item } = flatListItem;
// @ts-ignore
const { inviteItems } = this.state;
let selected = false;
let selected: IInvitee | IInviteSelectItem | undefined | boolean = false;
const renderableItem = this._getRenderableItem(flatListItem);
if (!renderableItem) {
@@ -570,7 +543,6 @@ class AddPeopleDialog
* @returns {void}
*/
_showFailedInviteAlert() {
// @ts-ignore
this.props.dispatch(openDialog(AlertDialog, {
contentKey: {
key: 'inviteDialog.alertText'
@@ -583,14 +555,15 @@ class AddPeopleDialog
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {any} _ownProps - Component's own props.
* @returns {{
* _isVisible: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
function _mapStateToProps(state: IReduxState, _ownProps: any) {
return {
..._abstractMapStateToProps(state)
};
}
// @ts-ignore
export default translate(connect(_mapStateToProps)(AddPeopleDialog));

View File

@@ -325,7 +325,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
const userTypes = [ INVITE_TYPES.USER, INVITE_TYPES.VIDEO_ROOM, INVITE_TYPES.ROOM ];
const users = response.filter(item => userTypes.includes(item.type));
const userDisplayItems = [];
const userDisplayItems: any = [];
for (const user of users) {
const { name, phone } = user;

View File

@@ -1,11 +1,11 @@
// @flow
import { Route } from '@react-navigation/native';
import React, { PureComponent } from 'react';
import { Linking, View } from 'react-native';
import { WithTranslation } from 'react-i18next';
import { Linking, View, ViewStyle } from 'react-native';
import { WebView } from 'react-native-webview';
import { connect } from 'react-redux';
import { type Dispatch } from 'redux';
import { IStore } from '../../../../app/types';
import { openDialog } from '../../../../base/dialog/actions';
import { translate } from '../../../../base/i18n/functions';
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
@@ -15,38 +15,32 @@ import { getDialInfoPageURLForURIString } from '../../../functions';
import DialInSummaryErrorDialog from './DialInSummaryErrorDialog';
import styles, { INDICATOR_COLOR } from './styles';
interface IProps extends WithTranslation {
type Props = {
dispatch: Dispatch<any>,
dispatch: IStore['dispatch'];
/**
* Default prop for navigating between screen components(React Navigation).
*/
navigation: Object,
navigation: any;
/**
* Default prop for navigating between screen components(React Navigation).
*/
route: Object,
/**
* Translation function.
*/
t: Function
};
route: Route<'', { summaryUrl: string; }>;
}
/**
* Implements a React native component that displays the dial in info page for a specific room.
*/
class DialInSummary extends PureComponent<Props> {
class DialInSummary extends PureComponent<IProps> {
/**
* Initializes a new instance.
*
* @inheritdoc
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this._onError = this._onError.bind(this);
@@ -86,15 +80,13 @@ class DialInSummary extends PureComponent<Props> {
onShouldStartLoadWithRequest = { this._onNavigate }
renderLoading = { this._renderLoading }
setSupportMultipleWindows = { false }
source = {{ uri: getDialInfoPageURLForURIString(summaryUrl) }}
source = {{ uri: getDialInfoPageURLForURIString(summaryUrl) ?? '' }}
startInLoadingState = { true }
style = { styles.webView } />
</JitsiScreen>
);
}
_onError: () => void;
/**
* Callback to handle the error if the page fails to load.
*
@@ -104,8 +96,6 @@ class DialInSummary extends PureComponent<Props> {
this.props.dispatch(openDialog(DialInSummaryErrorDialog));
}
_onNavigate: Object => Boolean;
/**
* Callback to intercept navigation inside the webview and make the native app handle the dial requests.
*
@@ -114,7 +104,7 @@ class DialInSummary extends PureComponent<Props> {
* @param {any} request - The request object.
* @returns {boolean}
*/
_onNavigate(request) {
_onNavigate(request: { url: string; }) {
const { url } = request;
const { route } = this.props;
const summaryUrl = route.params?.summaryUrl;
@@ -126,8 +116,6 @@ class DialInSummary extends PureComponent<Props> {
return url === getDialInfoPageURLForURIString(summaryUrl);
}
_renderLoading: () => React$Component<any>;
/**
* Renders the loading indicator.
*
@@ -135,7 +123,7 @@ class DialInSummary extends PureComponent<Props> {
*/
_renderLoading() {
return (
<View style = { styles.indicatorWrapper }>
<View style = { styles.indicatorWrapper as ViewStyle }>
<LoadingIndicator
color = { INDICATOR_COLOR }
size = 'large' />

View File

@@ -1,6 +1,5 @@
// @flow
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import AlertDialog from '../../../../base/dialog/components/native/AlertDialog';
@@ -9,7 +8,7 @@ import { translate } from '../../../../base/i18n/functions';
/**
* Dialog to inform the user that we couldn't fetch the dial-in info page.
*/
class DialInSummaryErrorDialog extends Component<{}> {
class DialInSummaryErrorDialog extends Component<WithTranslation> {
/**
* Implements React's {@link Component#render()}.
*
@@ -22,8 +21,6 @@ class DialInSummaryErrorDialog extends Component<{}> {
contentKey = 'info.dialInSummaryError' />
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(DialInSummaryErrorDialog));

View File

@@ -1,5 +1,3 @@
// @flow
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
export const INDICATOR_COLOR = BaseTheme.palette.ui07;

View File

@@ -31,7 +31,11 @@ const useStyles = makeStyles()((theme: Theme) => {
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
borderRadius: 6
borderRadius: 6,
'& *': {
userSelect: 'text'
}
},
confNameLabel: {
...withPixelLineHeight(theme.typography.heading6),

View File

@@ -0,0 +1,24 @@
/**
* The type of the action which signals that the keyboard shortcuts should be initialized.
*/
export const INIT_KEYBOARD_SHORTCUTS = 'INIT_KEYBOARD_SHORTCUTS';
/**
* The type of the action which signals that a keyboard shortcut should be registered.
*/
export const REGISTER_KEYBOARD_SHORTCUT = 'REGISTER_KEYBOARD_SHORTCUT';
/**
* The type of the action which signals that a keyboard shortcut should be unregistered.
*/
export const UNREGISTER_KEYBOARD_SHORTCUT = 'UNREGISTER_KEYBOARD_SHORTCUT';
/**
* The type of the action which signals that a keyboard shortcut should be enabled.
*/
export const ENABLE_KEYBOARD_SHORTCUTS = 'ENABLE_KEYBOARD_SHORTCUTS';
/**
* The type of the action which signals that a keyboard shortcut should be disabled.
*/
export const DISABLE_KEYBOARD_SHORTCUTS = 'DISABLE_KEYBOARD_SHORTCUTS';

View File

@@ -0,0 +1,60 @@
import { AnyAction } from 'redux';
import {
DISABLE_KEYBOARD_SHORTCUTS,
ENABLE_KEYBOARD_SHORTCUTS,
REGISTER_KEYBOARD_SHORTCUT,
UNREGISTER_KEYBOARD_SHORTCUT
} from './actionTypes';
import { IKeyboardShortcut } from './types';
/**
* Action to register a new shortcut.
*
* @param {IKeyboardShortcut} shortcut - The shortcut to register.
* @returns {AnyAction}
*/
export const registerShortcut = (shortcut: IKeyboardShortcut): AnyAction => {
return {
type: REGISTER_KEYBOARD_SHORTCUT,
shortcut
};
};
/**
* Action to unregister a shortcut.
*
* @param {string} character - The character of the shortcut to unregister.
* @param {boolean} altKey - Whether the shortcut used altKey.
* @returns {AnyAction}
*/
export const unregisterShortcut = (character: string, altKey = false): AnyAction => {
return {
altKey,
type: UNREGISTER_KEYBOARD_SHORTCUT,
character
};
};
/**
* Action to enable keyboard shortcuts.
*
* @returns {AnyAction}
*/
export const enableKeyboardShortcuts = (): AnyAction => {
return {
type: ENABLE_KEYBOARD_SHORTCUTS
};
};
/**
* Action to enable keyboard shortcuts.
*
* @returns {AnyAction}
*/
export const disableKeyboardShortcuts = (): AnyAction => {
return {
type: DISABLE_KEYBOARD_SHORTCUTS
};
};

View File

@@ -0,0 +1 @@
export * from './actions.any';

View File

@@ -0,0 +1,119 @@
import { batch } from 'react-redux';
import { ACTION_SHORTCUT_PRESSED, ACTION_SHORTCUT_RELEASED, createShortcutEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { clickOnVideo } from '../filmstrip/actions.web';
import { openSettingsDialog } from '../settings/actions.web';
import { SETTINGS_TABS } from '../settings/constants';
import { registerShortcut } from './actions.any';
import { areKeyboardShortcutsEnabled, getKeyboardShortcuts } from './functions';
import logger from './logger';
import { getKeyboardKey, getPriorityFocusedElement } from './utils';
export * from './actions.any';
/**
* Initialise global shortcuts.
* Global shortcuts are shortcuts for features that don't have a button or
* link associated with the action. In other words they represent actions
* triggered _only_ with a shortcut.
*
* @returns {Function}
*/
const initGlobalKeyboardShortcuts = () =>
(dispatch: IStore['dispatch']) => {
batch(() => {
dispatch(registerShortcut({
character: '?',
helpDescription: 'keyboardShortcuts.toggleShortcuts',
handler: () => {
sendAnalytics(createShortcutEvent('help'));
dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false));
}
}));
// register SPACE shortcut in two steps to insure visibility of help message
dispatch(registerShortcut({
character: ' ',
helpCharacter: 'SPACE',
helpDescription: 'keyboardShortcuts.pushToTalk',
handler: () => {
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_RELEASED));
logger.log('Talk shortcut released');
APP.conference.muteAudio(true);
}
}));
dispatch(registerShortcut({
character: '0',
helpDescription: 'keyboardShortcuts.focusLocal',
handler: () => {
dispatch(clickOnVideo(0));
}
}));
Array(9).fill(1)
.forEach((_, index) => {
const num = index + 1;
dispatch(registerShortcut({
character: `${num}`,
// only show help hint for the first shortcut
helpCharacter: num === 1 ? '1-9' : undefined,
helpDescription: num === 1 ? 'keyboardShortcuts.focusRemote' : undefined,
handler: () => {
dispatch(clickOnVideo(num));
}
}));
});
});
};
/**
* Initializes keyboard shortcuts.
*
* @returns {Function}
*/
export const initKeyboardShortcuts = () =>
(dispatch: IStore['dispatch'], getState: IStore['getState']) => {
dispatch(initGlobalKeyboardShortcuts());
window.onkeyup = (e: KeyboardEvent) => {
const state = getState();
const enabled = areKeyboardShortcutsEnabled(state);
const shortcuts = getKeyboardShortcuts(state);
if (!enabled || getPriorityFocusedElement()) {
return;
}
const key = getKeyboardKey(e).toUpperCase();
if (shortcuts.has(key)) {
shortcuts.get(key)?.handler(e);
}
};
window.onkeydown = (e: KeyboardEvent) => {
const state = getState();
const enabled = areKeyboardShortcutsEnabled(state);
if (!enabled) {
return;
}
const focusedElement = getPriorityFocusedElement();
const key = getKeyboardKey(e).toUpperCase();
if (key === ' ' && !focusedElement) {
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_PRESSED));
logger.log('Talk shortcut pressed');
APP.conference.muteAudio(false);
} else if (key === 'ESCAPE') {
focusedElement?.blur();
}
};
};

View File

@@ -0,0 +1,31 @@
import { IReduxState } from '../app/types';
/**
* Returns whether or not the keyboard shortcuts are enabled.
*
* @param {Object} state - The redux state.
* @returns {boolean} - Whether or not the keyboard shortcuts are enabled.
*/
export function areKeyboardShortcutsEnabled(state: IReduxState) {
return state['features/keyboard-shortcuts'].enabled;
}
/**
* Returns the keyboard shortcuts map.
*
* @param {Object} state - The redux state.
* @returns {Map} - The keyboard shortcuts map.
*/
export function getKeyboardShortcuts(state: IReduxState) {
return state['features/keyboard-shortcuts'].shortcuts;
}
/**
* Returns the keyboard shortcuts help descriptions.
*
* @param {Object} state - The redux state.
* @returns {Map} - The keyboard shortcuts help descriptions.
*/
export function getKeyboardShortcutsHelpDescriptions(state: IReduxState) {
return state['features/keyboard-shortcuts'].shortcutsHelp;
}

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/keyboard-shortcuts');

View File

@@ -0,0 +1,39 @@
import { IStore } from '../app/types';
import { SET_CONFIG } from '../base/config/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { CAPTURE_EVENTS } from '../remote-control/actionTypes';
import { disableKeyboardShortcuts, enableKeyboardShortcuts } from './actions';
MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any) => {
const { dispatch } = store;
switch (action.type) {
case CAPTURE_EVENTS:
if (action.isCapturingEvents) {
dispatch(disableKeyboardShortcuts());
} else {
dispatch(enableKeyboardShortcuts());
}
return next(action);
case SET_CONFIG: {
const result = next(action);
const state = store.getState();
const { disableShortcuts } = state['features/base/config'];
if (disableShortcuts !== undefined) {
if (disableShortcuts) {
dispatch(disableKeyboardShortcuts());
} else {
dispatch(enableKeyboardShortcuts());
}
}
return result;
}
}
return next(action);
});

View File

@@ -0,0 +1,72 @@
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
DISABLE_KEYBOARD_SHORTCUTS,
ENABLE_KEYBOARD_SHORTCUTS,
REGISTER_KEYBOARD_SHORTCUT,
UNREGISTER_KEYBOARD_SHORTCUT
} from './actionTypes';
import { IKeyboardShortcutsState } from './types';
/**
* The redux subtree of this feature.
*/
const STORE_NAME = 'features/keyboard-shortcuts';
const defaultState = {
enabled: true,
shortcuts: new Map(),
shortcutsHelp: new Map()
};
PersistenceRegistry.register(STORE_NAME, {
enabled: true
});
ReducerRegistry.register<IKeyboardShortcutsState>(STORE_NAME,
(state = defaultState, action): IKeyboardShortcutsState => {
switch (action.type) {
case ENABLE_KEYBOARD_SHORTCUTS:
return {
...state,
enabled: true
};
case DISABLE_KEYBOARD_SHORTCUTS:
return {
...state,
enabled: false
};
case REGISTER_KEYBOARD_SHORTCUT: {
const shortcutKey = action.shortcut.alt ? `:${action.shortcut.character}` : action.shortcut.character;
return {
...state,
shortcuts: new Map(state.shortcuts)
.set(shortcutKey, action.shortcut),
shortcutsHelp: action.shortcut.helpDescription
? new Map(state.shortcutsHelp)
.set(action.shortcut.helpCharacter ?? shortcutKey, action.shortcut.helpDescription)
: state.shortcutsHelp
};
}
case UNREGISTER_KEYBOARD_SHORTCUT: {
const shortcutKey = action.alt ? `:${action.character}` : action.character;
const shortcuts = new Map(state.shortcuts);
shortcuts.delete(shortcutKey);
const shortcutsHelp = new Map(state.shortcutsHelp);
shortcutsHelp.delete(shortcutKey);
return {
...state,
shortcuts,
shortcutsHelp
};
}
}
return state;
});

View File

@@ -0,0 +1,23 @@
export interface IKeyboardShortcut {
// whether or not the alt key must be pressed
alt?: boolean;
// the character to be pressed that triggers the action
character: string;
// the function to be executed when the shortcut is pressed
handler: Function;
// character to be displayed in the help dialog shortcuts list
helpCharacter?: string;
// help description of the shortcut, to be displayed in the help dialog
helpDescription?: string;
}
export interface IKeyboardShortcutsState {
enabled: boolean;
shortcuts: Map<string, IKeyboardShortcut>;
shortcutsHelp: Map<string, string>;
}

View File

@@ -0,0 +1,75 @@
/**
* Prefer keyboard handling of these elements over global shortcuts.
* If a button is triggered using the Spacebar it should not trigger PTT.
* If an input element is focused and M is pressed it should not mute audio.
*/
const _elementsBlacklist = [
'input',
'textarea',
'button',
'[role=button]',
'[role=menuitem]',
'[role=radio]',
'[role=tab]',
'[role=option]',
'[role=switch]',
'[role=range]',
'[role=log]'
];
/**
* Returns the currently focused element if it is not blacklisted.
*
* @returns {HTMLElement|null} - The currently focused element.
*/
export const getPriorityFocusedElement = (): HTMLElement | null =>
document.querySelector(`:focus:is(${_elementsBlacklist.join(',')})`);
/**
* Returns the keyboard key from a KeyboardEvent.
*
* @param {KeyboardEvent} e - The KeyboardEvent.
* @returns {string} - The keyboard key.
*/
export const getKeyboardKey = (e: KeyboardEvent): string => {
// @ts-ignore
const { altKey, code, key, shiftKey, type, which } = e;
// If alt is pressed a different char can be returned so this takes
// the char from the code. It also prefixes with a colon to differentiate
// alt combo from simple keypress.
if (altKey) {
const replacedKey = code.replace('Key', '');
return `:${replacedKey}`;
}
// If e.key is a string, then it is assumed it already plainly states
// the key pressed. This may not be true in all cases, such as with Edge
// and "?", when the browser cannot properly map a key press event to a
// keyboard key. To be safe, when a key is "Unidentified" it must be
// further analyzed by jitsi to a key using e.which.
if (typeof key === 'string' && key !== 'Unidentified') {
return key;
}
if (type === 'keypress'
&& ((which >= 32 && which <= 126)
|| (which >= 160 && which <= 255))) {
return String.fromCharCode(which);
}
// try to fallback (0-9A-Za-z and QWERTY keyboard)
switch (which) {
case 27:
return 'Escape';
case 191:
return shiftKey ? '?' : '/';
}
if (shiftKey || type === 'keypress') {
return String.fromCharCode(which);
}
return String.fromCharCode(which).toLowerCase();
};

View File

@@ -1,77 +1,76 @@
// @flow
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import type { Dispatch } from 'redux';
import { IReduxState, IStore } from '../../app/types';
import { JitsiTrackEvents } from '../../base/lib-jitsi-meet';
import ParticipantView from '../../base/participants/components/ParticipantView.native';
import { getParticipantById, isLocalScreenshareParticipant } from '../../base/participants/functions';
import { trackStreamingStatusChanged } from '../../base/tracks/actions.native';
import { getVideoTrackByParticipant, isLocalVideoTrackDesktop } from '../../base/tracks/functions.native';
import { ITrack } from '../../base/tracks/types';
import { AVATAR_SIZE } from './styles';
/**
* The type of the React {@link Component} props of {@link LargeVideo}.
*/
type Props = {
interface IProps {
/**
* Whether video should be disabled.
*/
_disableVideo: boolean,
_disableVideo: boolean;
/**
* Application's viewport height.
*/
_height: number,
_height: number;
/**
* The ID of the participant (to be) depicted by LargeVideo.
*
* @private
*/
_participantId: string,
_participantId: string;
/**
* The video track that will be displayed in the thumbnail.
*/
_videoTrack: ?Object,
_videoTrack?: ITrack;
/**
* Application's viewport height.
*/
_width: number,
_width: number;
/**
* Invoked to trigger state changes in Redux.
*/
dispatch: Dispatch<any>,
dispatch: IStore['dispatch'];
/**
* Callback to invoke when the {@code LargeVideo} is clicked/pressed.
*/
onClick: Function,
};
onClick?: Function;
}
/**
* The type of the React {@link Component} state of {@link LargeVideo}.
*/
type State = {
interface IState {
/**
* Size for the Avatar. It will be dynamically adjusted based on the
* available size.
*/
avatarSize: number,
avatarSize: number;
/**
* Whether the connectivity indicator will be shown or not. It will be true
* by default, but it may be turned off if there is not enough space.
*/
useConnectivityInfoLabel: boolean
};
useConnectivityInfoLabel: boolean;
}
const DEFAULT_STATE = {
avatarSize: AVATAR_SIZE,
@@ -84,14 +83,14 @@ const DEFAULT_STATE = {
*
* @augments Component
*/
class LargeVideo extends PureComponent<Props, State> {
class LargeVideo extends PureComponent<IProps, IState> {
/**
* Creates new LargeVideo component.
*
* @param {Props} props - The props of the component.
* @param {IProps} props - The props of the component.
* @returns {LargeVideo}
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this.handleTrackStreamingStatusChanged = this.handleTrackStreamingStatusChanged.bind(this);
@@ -108,7 +107,7 @@ class LargeVideo extends PureComponent<Props, State> {
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: Props) {
static getDerivedStateFromProps(props: IProps) {
const { _height, _width } = props;
// Get the size, rounded to the nearest even number.
@@ -152,7 +151,7 @@ class LargeVideo extends PureComponent<Props, State> {
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps: Props) {
componentDidUpdate(prevProps: IProps) {
// TODO: after converting this component to a react function component,
// use a custom hook to update local track streaming status.
const { _videoTrack, dispatch } = this.props;
@@ -200,7 +199,7 @@ class LargeVideo extends PureComponent<Props, State> {
* @param {JitsiTrackStreamingStatus} streamingStatus - The updated track streaming status.
* @returns {void}
*/
handleTrackStreamingStatusChanged(jitsiTrack, streamingStatus) {
handleTrackStreamingStatusChanged(jitsiTrack: any, streamingStatus: string) {
this.props.dispatch(trackStreamingStatusChanged(jitsiTrack, streamingStatus));
}
@@ -240,11 +239,11 @@ class LargeVideo extends PureComponent<Props, State> {
*
* @param {Object} state - Redux state.
* @private
* @returns {Props}
* @returns {IProps}
*/
function _mapStateToProps(state) {
function _mapStateToProps(state: IReduxState) {
const { participantId } = state['features/large-video'];
const participant = getParticipantById(state, participantId);
const participant = getParticipantById(state, participantId ?? '');
const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
const videoTrack = getVideoTrackByParticipant(state, participant);
let disableVideo = false;
@@ -258,7 +257,7 @@ function _mapStateToProps(state) {
return {
_disableVideo: disableVideo,
_height: height,
_participantId: participantId,
_participantId: participantId ?? '',
_videoTrack: videoTrack,
_width: width
};

View File

@@ -6,7 +6,7 @@ import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import ChatInputBar from '../../../chat/components/native/ChatInputBar';
import MessageContainer from '../../../chat/components/native/MessageContainer';
import AbstractLobbyScreen, {
Props as AbstractProps,
IProps as AbstractProps,
_mapStateToProps as abstractMapStateToProps
} from '../AbstractLobbyScreen';
@@ -30,6 +30,7 @@ class LobbyChatScreen extends
return (
<JitsiScreen style = { styles.lobbyChatWrapper }>
{/* @ts-ignore */}
<MessageContainer messages = { _lobbyChatMessages } />
<ChatInputBar onSend = { this._onSendMessage } />
</JitsiScreen>

View File

@@ -1,7 +1,8 @@
import React, { ReactElement } from 'react';
import { Text, View } from 'react-native';
import React from 'react';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getConferenceName } from '../../../base/conference/functions';
import { translate } from '../../../base/i18n/functions';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
@@ -20,29 +21,28 @@ import { preJoinStyles } from '../../../prejoin/components/native/styles';
import AudioMuteButton from '../../../toolbox/components/AudioMuteButton';
import VideoMuteButton from '../../../toolbox/components/VideoMuteButton';
import AbstractLobbyScreen, {
Props as AbstractProps,
IProps as AbstractProps,
_mapStateToProps as abstractMapStateToProps } from '../AbstractLobbyScreen';
import styles from './styles';
type Props = AbstractProps & {
interface IProps extends AbstractProps {
/**
* The current aspect ratio of the screen.
*/
_aspectRatio: Symbol,
_aspectRatio: Symbol;
/**
* The room name.
*/
_roomName: string
_roomName: string;
}
/**
* Implements a waiting screen that represents the participant being in the lobby.
*/
class LobbyScreen extends AbstractLobbyScreen<Props> {
class LobbyScreen extends AbstractLobbyScreen<IProps> {
/**
* Implements {@code PureComponent#render}.
*
@@ -70,7 +70,7 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
style = { contentWrapperStyles }>
<BrandingImageBackground />
<View style = { largeVideoContainerStyles }>
<View style = { preJoinStyles.displayRoomNameBackdrop }>
<View style = { preJoinStyles.displayRoomNameBackdrop as ViewStyle }>
<Text
numberOfLines = { 1 }
style = { preJoinStyles.preJoinRoomName }>
@@ -79,7 +79,7 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
</View>
<LargeVideo />
</View>
<View style = { contentContainerStyles }>
<View style = { contentContainerStyles as ViewStyle }>
{ this._renderToolbarButtons() }
{ this._renderContent() }
</View>
@@ -87,32 +87,6 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
);
}
_getScreenTitleKey: () => string;
_onAskToJoin: () => void;
_onCancel: () => boolean;
_onChangeDisplayName: Object => void;
_onChangeEmail: Object => void;
_onChangePassword: Object => void;
_onEnableEdit: () => void;
_onJoinWithPassword: () => void;
_onSwitchToKnockMode: () => void;
_onSwitchToPasswordMode: () => void;
_renderContent: () => ReactElement;
_renderToolbarButtons: () => ReactElement;
_onNavigateToLobbyChat: () => void;
/**
* Navigates to the lobby chat screen.
*
@@ -137,7 +111,7 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
<LoadingIndicator
color = { BaseTheme.palette.icon01 }
style = { styles.loadingIndicator } />
<Text style = { styles.joiningMessage }>
<Text style = { styles.joiningMessage as TextStyle }>
{ this.props.t('lobby.joiningMessage') }
</Text>
{ this._renderStandardButtons() }
@@ -183,7 +157,6 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
return (
<Input
autoCapitalize = 'none'
autoCompleteType = 'off'
customStyles = {{ input: styles.customInput }}
error = { _passwordJoinFailed }
onChange = { this._onChangePassword }
@@ -225,7 +198,7 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
*/
_renderToolbarButtons() {
return (
<View style = { preJoinStyles.toolboxContainer }>
<View style = { preJoinStyles.toolboxContainer as ViewStyle }>
<AudioMuteButton
styles = { preJoinStyles.buttonStylesBorderless } />
<VideoMuteButton
@@ -244,7 +217,7 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
const { displayName } = this.state;
return (
<View style = { styles.formWrapper }>
<View style = { styles.formWrapper as ViewStyle }>
{
_knocking && _isLobbyChatActive
&& <Button
@@ -282,14 +255,14 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @param {IProps} ownProps - The own props of the component.
* @returns {{
* _aspectRatio: Symbol
* }}
*/
function _mapStateToProps(state: Object, ownProps: Props) {
function _mapStateToProps(state: IReduxState) {
return {
...abstractMapStateToProps(state, ownProps),
...abstractMapStateToProps(state),
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
_roomName: getConferenceName(state)
};

View File

@@ -1,6 +1,5 @@
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
export default {
lobbyChatWrapper: {

View File

@@ -82,6 +82,7 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: IProps) =
name = { screen.welcome.main }
options = { welcomeScreenOptions } />
<RootStack.Screen
// @ts-ignore
component = { DialInSummary }
name = { screen.dialInSummary }
options = { dialInSummaryScreenOptions } />

View File

@@ -10,7 +10,7 @@ export const lobbyNavigationContainerRef = React.createRef<NavigationContainerRe
* @param {Object} params - Params to pass to the destination route.
* @returns {Function}
*/
export function navigate(name: string, params: Object) {
export function navigate(name: string, params?: Object) {
return lobbyNavigationContainerRef.current?.navigate(name, params);
}

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Animated, Text, View } from 'react-native';
import { Animated, Text, TextStyle, View, ViewStyle } from 'react-native';
import Icon from '../../../base/icons/components/Icon';
import {
@@ -19,7 +19,6 @@ import { NOTIFICATION_ICON, NOTIFICATION_TYPE } from '../../constants';
import { INotificationProps } from '../../types';
import { NotificationsTransitionContext } from '../NotificationsTransition';
// @ts-ignore
import styles from './styles';
@@ -153,7 +152,7 @@ const Notification = ({
if (descriptionArray?.length) {
return (
<>
<Text style = { styles.contentTextTitle }>
<Text style = { styles.contentTextTitle as TextStyle }>
{titleText}
</Text>
{
@@ -170,7 +169,7 @@ const Notification = ({
}
return (
<Text style = { styles.contentTextTitle }>
<Text style = { styles.contentTextTitle as TextStyle }>
{titleText}
</Text>
);
@@ -186,12 +185,12 @@ const Notification = ({
{
opacity: notificationOpacityAnimation
}
] }>
] as ViewStyle[] }>
<View
style = { icon === NOTIFICATION_ICON.PARTICIPANTS
style = { (icon === NOTIFICATION_ICON.PARTICIPANTS
? styles.contentColumn
: styles.interactiveContentColumn }>
<View style = { styles.iconContainer }>
: styles.interactiveContentColumn) as ViewStyle }>
<View style = { styles.iconContainer as ViewStyle }>
<Icon
color = { ICON_COLOR[appearance as keyof typeof ICON_COLOR] }
size = { 24 }
@@ -202,7 +201,7 @@ const Notification = ({
style = { styles.contentContainer }>
{_renderContent()}
</View>
<View style = { styles.btnContainer }>
<View style = { styles.btnContainer as ViewStyle }>
{mapAppearanceToButtons()}
</View>
</View>

View File

@@ -10,8 +10,6 @@ import { areThereNotifications } from '../../functions';
import NotificationsTransition from '../NotificationsTransition';
import Notification from './Notification';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import styles from './styles';

View File

@@ -1,5 +1,3 @@
// @flow
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const contentColumn = {

View File

@@ -5,7 +5,6 @@ import Button from '../../../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../../../base/ui/constants.native';
import { createBreakoutRoom } from '../../../../../breakout-rooms/actions';
// @ts-ignore
import styles from './styles';
/**

View File

@@ -5,7 +5,6 @@ import Button from '../../../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../../../base/ui/constants.native';
import { autoAssignToBreakoutRooms } from '../../../../../breakout-rooms/actions';
// @ts-ignore
import styles from './styles';
/**

View File

@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity } from 'react-native';
import { TouchableOpacity, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
@@ -13,17 +13,18 @@ import { IconCloseLarge, IconRingGroup } from '../../../../../base/icons/svg';
import { isLocalParticipantModerator } from '../../../../../base/participants/functions';
import { closeBreakoutRoom, moveToRoom, removeBreakoutRoom } from '../../../../../breakout-rooms/actions';
import { getBreakoutRoomsConfig } from '../../../../../breakout-rooms/functions';
import { IRoom } from '../../../../../breakout-rooms/types';
import styles from '../../../native/styles';
type Props = {
interface IProps {
/**
* The room for which the menu is open.
*/
room: Object
room: IRoom;
}
const BreakoutRoomContextMenu = ({ room }: Props) => {
const BreakoutRoomContextMenu = ({ room }: IProps) => {
const dispatch = useDispatch();
const isLocalModerator = useSelector(isLocalParticipantModerator);
const { hideJoinRoomButton } = useSelector(getBreakoutRoomsConfig);
@@ -53,7 +54,7 @@ const BreakoutRoomContextMenu = ({ room }: Props) => {
!hideJoinRoomButton && (
<TouchableOpacity
onPress = { onJoinRoom }
style = { styles.contextMenuItem }>
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconRingGroup } />
@@ -65,7 +66,7 @@ const BreakoutRoomContextMenu = ({ room }: Props) => {
&& (room?.participants && Object.keys(room.participants).length > 0
? <TouchableOpacity
onPress = { onCloseBreakoutRoom }
style = { styles.contextMenuItem }>
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCloseLarge } />
@@ -73,7 +74,7 @@ const BreakoutRoomContextMenu = ({ room }: Props) => {
</TouchableOpacity>
: <TouchableOpacity
onPress = { onRemoveBreakoutRoom }
style = { styles.contextMenuItem }>
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCloseLarge } />

View File

@@ -1,27 +1,27 @@
// @flow
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../../../app/types';
import { isLocalParticipantModerator, isParticipantModerator } from '../../../../../base/participants/functions';
import { IRoom } from '../../../../../breakout-rooms/types';
import { showRoomParticipantMenu } from '../../../../actions.native';
import ParticipantItem from '../../../native/ParticipantItem';
type Props = {
interface IProps {
/**
* Participant to be displayed.
*/
item: Object,
item: any;
/**
* The room the participant is in.
*/
room: Object
};
room: IRoom;
}
const BreakoutRoomParticipantItem = ({ item, room }: Props) => {
const { defaultRemoteDisplayName } = useSelector(state => state['features/base/config']);
const BreakoutRoomParticipantItem = ({ item, room }: IProps) => {
const { defaultRemoteDisplayName = '' } = useSelector((state: IReduxState) => state['features/base/config']);
const moderator = useSelector(isLocalParticipantModerator);
const dispatch = useDispatch();
const onPress = useCallback(() => {

View File

@@ -4,6 +4,7 @@ import { FlatList } from 'react-native';
import { useDispatch } from 'react-redux';
import { openSheet } from '../../../../../base/dialog/actions';
import { IRoom } from '../../../../../breakout-rooms/types';
import { participantMatchesSearch } from '../../../../functions';
import CollapsibleList from '../../../native/CollapsibleList';
import styles from '../../../native/styles';
@@ -11,17 +12,17 @@ import styles from '../../../native/styles';
import BreakoutRoomContextMenu from './BreakoutRoomContextMenu';
import BreakoutRoomParticipantItem from './BreakoutRoomParticipantItem';
type Props = {
interface IProps {
/**
* Room to display.
*/
room: Object,
room: IRoom;
/**
* Participants search string.
*/
searchString: string
searchString: string;
}
/**
@@ -30,12 +31,11 @@ type Props = {
* @param {Object} item - The participant.
* @returns {string} - The user ID.
*/
function _keyExtractor(item: Object) {
function _keyExtractor(item: any) {
return item.jid;
}
export const CollapsibleRoom = ({ room, searchString }: Props) => {
export const CollapsibleRoom = ({ room, searchString }: IProps) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const _openContextMenu = useCallback(() => {
@@ -50,7 +50,7 @@ export const CollapsibleRoom = ({ room, searchString }: Props) => {
// a certain height percentage for every section in order for all to fit
// inside the participants pane container
const containerStyle
= roomParticipantsNr > 2 && styles.collapsibleRoomContainer;
= roomParticipantsNr > 2 ? styles.collapsibleRoomContainer : undefined;
return (
<CollapsibleList
@@ -62,11 +62,12 @@ export const CollapsibleRoom = ({ room, searchString }: Props) => {
data = { Object.values(room.participants || {}) }
horizontal = { false }
keyExtractor = { _keyExtractor }
// eslint-disable-next-line react/jsx-no-bind
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow
renderItem = { ({ item: participant }) => participantMatchesSearch(participant, searchString)
&& <BreakoutRoomParticipantItem
? <BreakoutRoomParticipantItem
item = { participant }
room = { room } /> }
room = { room } />
: null }
scrollEnabled = { true }
showsHorizontalScrollIndicator = { false }
windowSize = { 2 } />

View File

@@ -7,7 +7,6 @@ import Button from '../../../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../../../base/ui/constants.native';
import { moveToRoom } from '../../../../../breakout-rooms/actions';
// @ts-ignore
import styles from './styles';
/**

View File

@@ -1,4 +1,4 @@
import BaseTheme from '../../../../../base/ui/components/BaseTheme';
import BaseTheme from '../../../../../base/ui/components/BaseTheme.native';
const baseButton = {
borderRadius: BaseTheme.shape.borderRadius,

View File

@@ -1,37 +1,35 @@
// @flow
import React, { useCallback, useState } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { GestureResponderEvent, Text, TextStyle, TouchableOpacity, View, ViewStyle } from 'react-native';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
import { StyleType } from '../../../base/styles/functions.native';
import styles from '../breakout-rooms/components/native/styles';
type Props = {
interface IProps {
/**
* The children to be displayed within this list.
*/
children: React$Node,
children: React.ReactNode;
/**
* Additional style to be appended to the CollapsibleList container.
*/
containerStyle?: StyleType,
containerStyle?: StyleType;
/**
* Callback to invoke when the {@code CollapsibleList} is long pressed.
*/
onLongPress?: Function,
onLongPress?: (e?: GestureResponderEvent) => void;
/**
* Collapsible list title.
*/
title: Object
title: Object;
}
const CollapsibleList = ({ children, containerStyle, onLongPress, title }: Props) => {
const CollapsibleList = ({ children, containerStyle, onLongPress, title }: IProps) => {
const [ collapsed, setCollapsed ] = useState(false);
const _toggleCollapsed = useCallback(() => {
setCollapsed(!collapsed);
@@ -42,15 +40,15 @@ const CollapsibleList = ({ children, containerStyle, onLongPress, title }: Props
<TouchableOpacity
onLongPress = { onLongPress }
onPress = { _toggleCollapsed }
style = { styles.collapsibleList }>
style = { styles.collapsibleList as ViewStyle }>
<TouchableOpacity
onPress = { _toggleCollapsed }
style = { styles.arrowIcon }>
style = { styles.arrowIcon as ViewStyle }>
<Icon
size = { 18 }
src = { collapsed ? IconArrowDown : IconArrowUp } />
</TouchableOpacity>
<Text style = { styles.listTile }>
<Text style = { styles.listTile as TextStyle }>
{
title
}

View File

@@ -1,38 +1,42 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity, View } from 'react-native';
import { TouchableOpacity, View, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { Avatar } from '../../../base/avatar';
import Avatar from '../../../base/avatar/components/Avatar';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import Icon from '../../../base/icons/components/Icon';
import { IconCloseLarge } from '../../../base/icons/svg';
import { IParticipant } from '../../../base/participants/types';
import { setKnockingParticipantApproval } from '../../../lobby/actions.native';
import { getKnockingParticipantsById } from '../../../lobby/functions';
import styles from './styles';
type Props = {
interface IProps {
/**
* Participant reference.
*/
participant: Object
};
participant: IParticipant;
}
const ContextMenuLobbyParticipantReject = ({ participant: p }: Props) => {
const ContextMenuLobbyParticipantReject = ({ participant: p }: IProps) => {
const dispatch = useDispatch();
const knockParticipantsIDArr = useSelector(getKnockingParticipantsById);
const knockParticipantIsAvailable = knockParticipantsIDArr.find(knockPartId => knockPartId === p.id);
const displayName = p.name;
const reject = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, false), [ dispatch ]));
const reject = useCallback(() => {
dispatch(setKnockingParticipantApproval(p.id, false));
},
[ dispatch ]);
const { t } = useTranslation();
// eslint-disable-next-line react/no-multi-comp
const renderMenuHeader = () => (
<View
style = { styles.contextMenuItemSectionAvatar }>
style = { styles.contextMenuItemSectionAvatar as ViewStyle }>
<Avatar
participantId = { p.id }
size = { 24 } />
@@ -47,11 +51,10 @@ const ContextMenuLobbyParticipantReject = ({ participant: p }: Props) => {
addScrollViewPadding = { false }
/* eslint-disable-next-line react/jsx-no-bind */
renderHeader = { renderMenuHeader }
showSlidingView = { Boolean(knockParticipantIsAvailable) }
style = { styles.contextMenuMore }>
showSlidingView = { Boolean(knockParticipantIsAvailable) }>
<TouchableOpacity
onPress = { reject }
style = { styles.contextMenuItem }>
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCloseLarge } />

View File

@@ -1,9 +1,10 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity, View } from 'react-native';
import { TouchableOpacity, View, ViewStyle } from 'react-native';
import { Divider, Text } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import {
requestDisableAudioModeration,
requestDisableVideoModeration,
@@ -33,7 +34,7 @@ export const ContextMenuMore = () => {
}, [ dispatch ]);
const { t } = useTranslation();
const isModerationSupported = useSelector(isAvModerationSupported);
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
const allModerators = useSelector(isEveryoneModerator);
const participantCount = useSelector(getParticipantCount);
@@ -52,28 +53,29 @@ export const ContextMenuMore = () => {
showSlidingView = { true }>
<TouchableOpacity
onPress = { muteAllVideo }
style = { styles.contextMenuItem }>
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconVideoOff } />
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
</TouchableOpacity>
{isModerationSupported && ((participantCount === 1 || !allModerators)) && <>
{/* @ts-ignore */}
<Divider style = { styles.divider } />
<View style = { styles.contextMenuItem }>
<View style = { styles.contextMenuItem as ViewStyle }>
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.allow')}</Text>
</View>
{isAudioModerationEnabled
? <TouchableOpacity
onPress = { disableAudioModeration }
style = { styles.contextMenuItem }>
style = { styles.contextMenuItem as ViewStyle }>
<Text style = { styles.contextMenuItemTextNoIcon }>
{t('participantsPane.actions.audioModeration')}
</Text>
</TouchableOpacity>
: <TouchableOpacity
onPress = { enableAudioModeration }
style = { styles.contextMenuItem }>
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCheck } />
@@ -84,14 +86,14 @@ export const ContextMenuMore = () => {
{isVideoModerationEnabled
? <TouchableOpacity
onPress = { disableVideoModeration }
style = { styles.contextMenuItem }>
style = { styles.contextMenuItem as ViewStyle }>
<Text style = { styles.contextMenuItemTextNoIcon }>
{t('participantsPane.actions.videoModeration')}
</Text>
</TouchableOpacity>
: <TouchableOpacity
onPress = { enableVideoModeration }
style = { styles.contextMenuItem }>
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCheck } />

View File

@@ -1,6 +1,7 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { IParticipant } from '../../../base/participants/types';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { setKnockingParticipantApproval } from '../../../lobby/actions.native';
@@ -8,23 +9,22 @@ import { setKnockingParticipantApproval } from '../../../lobby/actions.native';
import ParticipantItem from './ParticipantItem';
import styles from './styles';
type Props = {
interface IProps {
/**
* Participant reference.
*/
participant: Object
};
participant: IParticipant;
}
export const LobbyParticipantItem = ({ participant: p }: Props) => {
export const LobbyParticipantItem = ({ participant: p }: IProps) => {
const dispatch = useDispatch();
const admit = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, true), [ dispatch ]));
const reject = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, false), [ dispatch ]));
const admit = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, true)), [ dispatch ]);
const reject = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, false)), [ dispatch ]);
return (
<ParticipantItem
displayName = { p.name }
displayName = { p.name ?? '' }
isKnockingParticipant = { true }
key = { p.id }
participantID = { p.id } >

View File

@@ -1,8 +1,6 @@
// @flow
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, Text, View } from 'react-native';
import { ScrollView, Text, TextStyle, View, ViewStyle } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import Button from '../../../base/ui/components/native/Button';
@@ -14,7 +12,6 @@ import CollapsibleList from './CollapsibleList';
import { LobbyParticipantItem } from './LobbyParticipantItem';
import styles from './styles';
const LobbyParticipantList = () => {
const lobbyEnabled = useSelector(getLobbyEnabled);
const participants = useSelector(getKnockingParticipants);
@@ -30,8 +27,8 @@ const LobbyParticipantList = () => {
}
const title = (
<View style = { styles.lobbyListDetails } >
<Text style = { styles.lobbyListDescription }>
<View style = { styles.lobbyListDetails as ViewStyle } >
<Text style = { styles.lobbyListDescription as TextStyle }>
{ t('participantsPane.headings.waitingLobby',
{ count: participants.length }) }
</Text>

View File

@@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import {
getLocalParticipant,
@@ -9,7 +10,7 @@ import {
hasRaisedHand,
isParticipantModerator
} from '../../../base/participants/functions';
import { FakeParticipant } from '../../../base/participants/types';
import { FakeParticipant, IParticipant } from '../../../base/participants/types';
import {
isParticipantAudioMuted,
isParticipantVideoMuted
@@ -20,88 +21,85 @@ import { getParticipantAudioMediaState, getParticipantVideoMediaState } from '..
import ParticipantItem from './ParticipantItem';
type Props = {
interface IProps {
/**
* Media state for audio.
*/
_audioMediaState: MediaState,
_audioMediaState: MediaState;
/**
* Whether or not to disable the moderator indicator.
*/
_disableModeratorIndicator: boolean,
_disableModeratorIndicator?: boolean;
/**
* The display name of the participant.
*/
_displayName: string,
_displayName: string;
/**
* The type of fake participant.
*/
_fakeParticipant: FakeParticipant,
_fakeParticipant: FakeParticipant;
/**
* Whether or not the user is a moderator.
*/
_isModerator: boolean,
_isModerator: boolean;
/**
* True if the participant is the local participant.
*/
_local: boolean,
_local: boolean;
/**
* Shared video local participant owner.
*/
_localVideoOwner: boolean,
_localVideoOwner: boolean;
/**
* The participant ID.
*/
_participantID: string,
_participantID: string;
/**
* True if the participant have raised hand.
*/
_raisedHand: boolean,
_raisedHand: boolean;
/**
* Media state for video.
*/
_videoMediaState: MediaState,
_videoMediaState: MediaState;
/**
* The redux dispatch function.
*/
dispatch: Function,
dispatch: Function;
/**
* The participant.
*/
participant: ?Object
};
participant?: IParticipant;
}
/**
* Implements the MeetingParticipantItem component.
*/
class MeetingParticipantItem extends PureComponent<Props> {
class MeetingParticipantItem extends PureComponent<IProps> {
/**
* Creates new MeetingParticipantItem instance.
*
* @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._onPress = this._onPress.bind(this);
}
_onPress: () => void;
/**
* Handles MeetingParticipantItem press events.
*
@@ -166,12 +164,12 @@ class MeetingParticipantItem extends PureComponent<Props> {
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {Props}
* @returns {IProps}
*/
function mapStateToProps(state, ownProps): Object {
function mapStateToProps(state: IReduxState, ownProps: any) {
const { participant } = ownProps;
const { ownerId } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state).id;
const localParticipantId = getLocalParticipant(state)?.id;
const _isAudioMuted = Boolean(participant && isParticipantAudioMuted(participant, state));
const _isVideoMuted = isParticipantVideoMuted(participant, state);
const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);

View File

@@ -78,7 +78,7 @@ interface IProps extends WithTranslation {
/**
* The remote participants.
*/
_sortedRemoteParticipants: Map<string, string>;
_sortedRemoteParticipants: string[];
/**
* The current visitors count if any.
@@ -229,7 +229,7 @@ class MeetingParticipantList extends PureComponent<IProps> {
= isLocalModerator
? containerStyleModerator : styles.notLocalModeratorContainer;
const finalContainerStyle
= _participantsCount > 6 && containerStyle;
= _participantsCount > 6 ? containerStyle : undefined;
const { color, shareDialogVisible } = _inviteOthersControl;
const _visitorsLabelText = _visitorsCount > 0
? t('participantsPane.headings.visitors', { count: _visitorsCount })
@@ -289,7 +289,7 @@ class MeetingParticipantList extends PureComponent<IProps> {
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState): Object {
function _mapStateToProps(state: IReduxState) {
const _participantsCount = getParticipantCountWithFake(state);
const { remoteParticipants } = state['features/filmstrip'];
const { shareDialogVisible } = state['features/share-room'];

View File

@@ -1,7 +1,6 @@
import React from 'react';
import type { Node } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity, View } from 'react-native';
import { GestureResponderEvent, TextStyle, TouchableOpacity, View, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import Avatar from '../../../base/avatar/components/Avatar';
@@ -10,62 +9,62 @@ import { AudioStateIcons, MEDIA_STATE, type MediaState, VideoStateIcons } from '
import { RaisedHandIndicator } from './RaisedHandIndicator';
import styles from './styles';
type Props = {
interface IProps {
/**
* Media state for audio.
*/
audioMediaState?: MediaState,
audioMediaState?: MediaState;
/**
* React children.
*/
children?: Node,
children?: React.ReactNode;
/**
* Whether or not to disable the moderator indicator.
*/
disableModeratorIndicator?: boolean,
disableModeratorIndicator?: boolean;
/**
* The name of the participant. Used for showing lobby names.
*/
displayName: string,
displayName: string;
/**
* Is the participant waiting?
*/
isKnockingParticipant: boolean,
isKnockingParticipant?: boolean;
/**
* Whether or not the user is a moderator.
*/
isModerator?: boolean,
isModerator?: boolean;
/**
* True if the participant is local.
*/
local?: boolean,
local?: boolean;
/**
* Callback to be invoked on pressing the participant item.
*/
onPress?: Function,
onPress?: (e?: GestureResponderEvent) => void;
/**
* The ID of the participant.
*/
participantID: string,
participantID: string;
/**
* True if the participant have raised hand.
*/
raisedHand?: boolean,
raisedHand?: boolean;
/**
* Media state for video.
*/
videoMediaState?: MediaState
videoMediaState?: MediaState;
}
/**
@@ -85,15 +84,15 @@ function ParticipantItem({
raisedHand,
audioMediaState = MEDIA_STATE.NONE,
videoMediaState = MEDIA_STATE.NONE
}: Props) {
}: IProps) {
const { t } = useTranslation();
return (
<View style = { styles.participantContainer } >
<View style = { styles.participantContainer as ViewStyle } >
<TouchableOpacity
onPress = { onPress }
style = { styles.participantContent }>
style = { styles.participantContent as ViewStyle }>
<Avatar
displayName = { displayName }
participantId = { participantID }
@@ -103,23 +102,23 @@ function ParticipantItem({
styles.participantDetailsContainer,
raisedHand && styles.participantDetailsContainerRaisedHand
] }>
<View style = { styles.participantNameContainer }>
<View style = { styles.participantNameContainer as ViewStyle }>
<Text
numberOfLines = { 1 }
style = { styles.participantName }>
style = { styles.participantName as TextStyle }>
{ displayName }
{ local && ` (${t('chat.you')})` }
</Text>
</View>
{ isModerator && !disableModeratorIndicator
&& <Text style = { styles.moderatorLabel }>{ t('videothumbnail.moderator') }</Text>
&& <Text style = { styles.moderatorLabel as TextStyle }>{ t('videothumbnail.moderator') }</Text>
}
</View>
{
!isKnockingParticipant
&& <>
{ raisedHand && <RaisedHandIndicator /> }
<View style = { styles.participantStatesContainer }>
<View style = { styles.participantStatesContainer as ViewStyle }>
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
<View>{ AudioStateIcons[audioMediaState] }</View>
</View>

View File

@@ -1,8 +1,7 @@
// @flow
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { equals } from '../../../base/redux/functions';
@@ -24,7 +23,6 @@ import MeetingParticipantList from './MeetingParticipantList';
import ParticipantsPaneFooter from './ParticipantsPaneFooter';
import styles from './styles';
/**
* Participants pane.
*
@@ -33,12 +31,12 @@ import styles from './styles';
const ParticipantsPane = () => {
const [ searchString, setSearchString ] = useState('');
const isLocalModerator = useSelector(isLocalParticipantModerator);
const { conference } = useSelector(state => state['features/base/conference']);
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
const _isBreakoutRoomsSupported = conference?.getBreakoutRooms()?.isSupported();
const currentRoomId = useSelector(getCurrentRoomId);
const rooms: Array<Object> = Object.values(useSelector(getBreakoutRooms, equals))
.filter((room: Object) => room.id !== currentRoomId)
.sort((p1: Object, p2: Object) => (p1?.name || '').localeCompare(p2?.name || ''));
const rooms = Object.values(useSelector(getBreakoutRooms, equals))
.filter(room => room.id !== currentRoomId)
.sort((p1, p2) => (p1?.name || '').localeCompare(p2?.name || ''));
const inBreakoutRoom = useSelector(isInBreakoutRoom);
const showAddBreakoutRoom = useSelector(isAddBreakoutRoomButtonVisible);
const showAutoAssign = useSelector(isAutoAssignParticipantsVisible);
@@ -46,7 +44,7 @@ const ParticipantsPane = () => {
return (
<JitsiScreen
footerComponent = { isLocalModerator && ParticipantsPaneFooter }
footerComponent = { isLocalModerator ? ParticipantsPaneFooter : undefined }
style = { styles.participantsPaneContainer }>
<LobbyParticipantList />
<MeetingParticipantList

View File

@@ -1,7 +1,4 @@
// @flow
import { connect } from 'react-redux';
import type { Dispatch } from 'redux';
import { translate } from '../../../base/i18n/functions';
import { IconUsers } from '../../../base/icons/svg';
@@ -10,19 +7,11 @@ import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Dispatch<any>
};
/**
* Implements an {@link AbstractButton} to open the participants panel.
*/
class ParticipantsPaneButton extends AbstractButton<Props, *> {
class ParticipantsPaneButton extends AbstractButton<AbstractButtonProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.participants';
icon = IconUsers;
label = 'toolbar.participants';

View File

@@ -1,6 +1,6 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { View } from 'react-native';
import { View, ViewStyle } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { openDialog, openSheet } from '../../../base/dialog/actions';
@@ -32,7 +32,7 @@ const ParticipantsPaneFooter = (): JSX.Element => {
const showMuteAll = useSelector(isMuteAllVisible);
return (
<View style = { styles.participantsPaneFooter }>
<View style = { styles.participantsPaneFooter as ViewStyle }>
{
showMuteAll && (
<Button

View File

@@ -1,7 +1,7 @@
/* eslint-disable lines-around-comment */
import React, { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { Text, View } from 'react-native';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
@@ -85,8 +85,8 @@ class RoomParticipantMenu extends PureComponent<IProps> {
<BottomSheet
renderHeader = { this._renderMenuHeader }
showSlidingView = { true }>
<View style = { styles.contextMenuItem }>
<Text style = { styles.contextMenuItemText }>
<View style = { styles.contextMenuItem as ViewStyle }>
<Text style = { styles.contextMenuItemText as ViewStyle }>
{t('breakoutRooms.actions.sendToBreakoutRoom')}
</Text>
</View>
@@ -120,11 +120,11 @@ class RoomParticipantMenu extends PureComponent<IProps> {
<View
style = { [
bottomSheetStyles.sheet,
styles.participantNameContainer ] }>
styles.participantNameContainer ] as ViewStyle[] }>
<Avatar
displayName = { participantName }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel }>
<Text style = { styles.participantNameLabel as TextStyle }>
{ participantName }
</Text>
</View>

View File

@@ -1,5 +1,6 @@
import React, { ComponentType, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { GestureResponderEvent } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { createPollEvent } from '../../analytics/AnalyticsEvents';
@@ -33,13 +34,13 @@ export type AnswerInfo = {
*/
export type AbstractProps = {
answers: Array<AnswerInfo>;
changeVote: (e: React.MouseEvent) => void;
changeVote: (e?: React.MouseEvent<HTMLButtonElement> | GestureResponderEvent) => void;
creatorName: string;
haveVoted: boolean;
question: string;
showDetails: boolean;
t: Function;
toggleIsDetailed: (e: React.MouseEvent) => void;
toggleIsDetailed: (e?: React.MouseEvent<HTMLButtonElement> | GestureResponderEvent) => void;
};
/**

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Text, View } from 'react-native';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { getLocalParticipant } from '../../../base/participants/functions';
@@ -29,25 +29,25 @@ const PollAnswer = (props: AbstractProps) => {
return (
<>
<Text style = { dialogStyles.questionText } >{ poll.question }</Text>
<Text style = { dialogStyles.questionOwnerText } >{
<Text style = { dialogStyles.questionText as TextStyle } >{ poll.question }</Text>
<Text style = { dialogStyles.questionOwnerText as TextStyle } >{
t('polls.by', { name: localParticipant?.name })
}
</Text>
<View style = { chatStyles.answerContent }>
<View style = { chatStyles.answerContent as ViewStyle }>
{poll.answers.map((answer, index) => (
<View
key = { index }
style = { chatStyles.switchRow } >
style = { chatStyles.switchRow as ViewStyle } >
<Switch
checked = { checkBoxStates[index] }
/* eslint-disable-next-line react/jsx-no-bind */
onChange = { state => setCheckbox(index, state) } />
<Text style = { chatStyles.switchLabel }>{answer.name}</Text>
<Text style = { chatStyles.switchLabel as TextStyle }>{answer.name}</Text>
</View>
))}
</View>
<View style = { chatStyles.buttonRow }>
<View style = { chatStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.skip'
labelKey = 'polls.answer.skip'

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FlatList, Platform, View } from 'react-native';
import { FlatList, Platform, View, ViewStyle } from 'react-native';
import { TextInput } from 'react-native-gesture-handler';
import { Divider } from 'react-native-paper';
import Button from '../../../base/ui/components/native/Button';
@@ -8,12 +9,10 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import styles
from '../../../settings/components/native/styles';
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
import AbstractPollCreate from '../AbstractPollCreate';
import type { AbstractProps } from '../AbstractPollCreate';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
import { chatStyles, dialogStyles } from './styles';
const PollCreate = (props: AbstractProps) => {
const {
addAnswer,
@@ -28,13 +27,13 @@ const PollCreate = (props: AbstractProps) => {
t
} = props;
const answerListRef = useRef(null);
const answerListRef = useRef<FlatList>(null);
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
* This array is maintained by registerFieldRef and the useEffect below.
*/
const answerInputs = useRef([]);
const answerInputs = useRef<TextInput[]>([]);
const registerFieldRef = useCallback((i, input) => {
if (input === null) {
return;
@@ -51,7 +50,7 @@ const PollCreate = (props: AbstractProps) => {
* This state allows us to requestFocus asynchronously, without having to worry
* about whether a newly created input field has been rendered yet or not.
*/
const [ lastFocus, requestFocus ] = useState(null);
const [ lastFocus, requestFocus ] = useState<number | null>(null);
const { PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;
useEffect(() => {
@@ -70,7 +69,7 @@ const PollCreate = (props: AbstractProps) => {
const onQuestionKeyDown = useCallback(() => {
answerInputs.current[0].focus();
});
}, []);
// Called on keypress in answer fields
const onAnswerKeyDown = useCallback((index: number, ev) => {
@@ -84,7 +83,7 @@ const PollCreate = (props: AbstractProps) => {
}, [ answers, addAnswer, removeAnswer, requestFocus ]);
/* eslint-disable react/no-multi-comp */
const createRemoveOptionButton = onPress => (
const createRemoveOptionButton = (onPress: () => void) => (
<Button
labelKey = 'polls.create.removeOption'
labelStyle = { dialogStyles.optionRemoveButtonText }
@@ -95,12 +94,12 @@ const PollCreate = (props: AbstractProps) => {
/* eslint-disable react/jsx-no-bind */
const renderListItem = ({ index }: { index: number }) =>
const renderListItem = ({ index }: { index: number; }) =>
// padding to take into account the two default options
(
<View
style = { dialogStyles.optionContainer }>
style = { dialogStyles.optionContainer as ViewStyle }>
<Input
blurOnSubmit = { false }
label = { t('polls.create.pollOption', { index: index + 1 }) }
@@ -124,8 +123,8 @@ const PollCreate = (props: AbstractProps) => {
? chatStyles.pollCreateButtonsContainerAndroid : chatStyles.pollCreateButtonsContainerIos;
return (
<View style = { chatStyles.pollCreateContainer }>
<View style = { chatStyles.pollCreateSubContainer }>
<View style = { chatStyles.pollCreateContainer as ViewStyle }>
<View style = { chatStyles.pollCreateSubContainer as ViewStyle }>
<Input
autoFocus = { true }
blurOnSubmit = { false }
@@ -140,15 +139,15 @@ const PollCreate = (props: AbstractProps) => {
// This is set to help the touch event not be propagated to any subviews.
pointerEvents = { 'auto' }
value = { question } />
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<FlatList
blurOnSubmit = { true }
data = { answers }
extraData = { answers }
keyExtractor = { (item, index) => index.toString() }
ref = { answerListRef }
renderItem = { renderListItem } />
<View style = { pollCreateButtonsContainerStyles }>
<View style = { pollCreateButtonsContainerStyles as ViewStyle }>
<Button
accessibilityLabel = 'polls.create.addOption'
disabled = { answers.length >= ANSWERS_LIMIT }
@@ -161,7 +160,7 @@ const PollCreate = (props: AbstractProps) => {
style = { chatStyles.pollCreateAddButton }
type = { SECONDARY } />
<View
style = { chatStyles.buttonRow }>
style = { chatStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.create.cancel'
labelKey = 'polls.create.cancel'

View File

@@ -1,7 +1,5 @@
// @flow
import React from 'react';
import { View } from 'react-native';
import { View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { shouldShowResults } from '../../functions';
@@ -10,21 +8,21 @@ import PollAnswer from './PollAnswer';
import PollResults from './PollResults';
import { chatStyles } from './styles';
type Props = {
interface IProps {
/**
* Id of the poll.
*/
pollId: string,
pollId: string;
}
const PollItem = ({ pollId }: Props) => {
const PollItem = ({ pollId }: IProps) => {
const showResults = useSelector(shouldShowResults(pollId));
return (
<View
style = { chatStyles.pollItemContainer }>
style = { chatStyles.pollItemContainer as ViewStyle }>
{ showResults
? <PollResults
key = { pollId }

View File

@@ -1,7 +1,5 @@
// @flow
import React, { useCallback } from 'react';
import { FlatList, Text, View } from 'react-native';
import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { getLocalParticipant } from '../../../base/participants/functions';
@@ -12,7 +10,6 @@ import type { AbstractProps, AnswerInfo } from '../AbstractPollResults';
import { chatStyles, dialogStyles, resultsStyles } from './styles';
/**
* Component that renders the poll results.
*
@@ -39,10 +36,10 @@ const PollResults = (props: AbstractProps) => {
* @returns {React.Node}
*/
const renderHeader = (answer: string, percentage: number, nbVotes: number) => (
<View style = { resultsStyles.answerHeader }>
<Text style = { resultsStyles.answer }>{ answer }</Text>
<View style = { resultsStyles.answerHeader as ViewStyle }>
<Text style = { resultsStyles.answer as TextStyle }>{ answer }</Text>
<View>
<Text style = { resultsStyles.answer }>({nbVotes}) {percentage}%</Text>
<Text style = { resultsStyles.answer as TextStyle }>({nbVotes}) {percentage}%</Text>
</View>
</View>
);
@@ -58,21 +55,22 @@ const PollResults = (props: AbstractProps) => {
if (showDetails) {
return (
<View style = { resultsStyles.answerContainer }>
<View style = { resultsStyles.answerContainer as ViewStyle }>
{ renderHeader(name, percentage, voterCount) }
<View style = { resultsStyles.barContainer }>
<View style = { [ resultsStyles.bar, { width: `${percentage}%` } ] } />
<View style = { resultsStyles.barContainer as ViewStyle }>
<View style = { [ resultsStyles.bar, { width: `${percentage}%` } ] as ViewStyle[] } />
</View>
{ voters && voterCount > 0
&& <View style = { resultsStyles.voters }>
{voters.map(({ id, name: voterName }) =>
(<Text
key = { id }
style = { resultsStyles.voter }>
{ voterName }
</Text>)
)}
</View>}
&& <View style = { resultsStyles.voters as ViewStyle }>
{/* @ts-ignore */}
{voters.map(({ id, name: voterName }) =>
(<Text
key = { id }
style = { resultsStyles.voter as TextStyle }>
{ voterName }
</Text>)
)}
</View>}
</View>
);
}
@@ -81,10 +79,10 @@ const PollResults = (props: AbstractProps) => {
// else, we display a simple list
// We add a progress bar by creating an empty view of width equal to percentage.
return (
<View style = { resultsStyles.answerContainer }>
<View style = { resultsStyles.answerContainer as ViewStyle }>
{ renderHeader(answer.name, percentage, voterCount) }
<View style = { resultsStyles.barContainer }>
<View style = { [ resultsStyles.bar, { width: `${percentage}%` } ] } />
<View style = { resultsStyles.barContainer as ViewStyle }>
<View style = { [ resultsStyles.bar, { width: `${percentage}%` } ] as ViewStyle[] } />
</View>
</View>
);
@@ -96,13 +94,15 @@ const PollResults = (props: AbstractProps) => {
/* eslint-disable react/jsx-no-bind */
return (
<View>
<Text style = { dialogStyles.questionText } >{ question }</Text>
<Text style = { dialogStyles.questionOwnerText } >{ t('polls.by', { name: localParticipant.name }) }</Text>
<Text style = { dialogStyles.questionText as TextStyle } >{ question }</Text>
<Text style = { dialogStyles.questionOwnerText as TextStyle } >
{ t('polls.by', { name: localParticipant?.name }) }
</Text>
<FlatList
data = { answers }
keyExtractor = { (item, index) => index.toString() }
renderItem = { answer => renderRow(answer.item) } />
<View style = { chatStyles.bottomLinks }>
<View style = { chatStyles.bottomLinks as ViewStyle }>
<Button
labelKey = {
showDetails

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