mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-13 18:20:18 +00:00
Compare commits
35 Commits
rm-dead-co
...
7011
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0fe034db5 | ||
|
|
8225f5e363 | ||
|
|
c641835d0f | ||
|
|
35ee92869f | ||
|
|
9b7a5ffdd1 | ||
|
|
581c2e621c | ||
|
|
f3117f3037 | ||
|
|
877ef58dfb | ||
|
|
19e61747b8 | ||
|
|
b4bf363237 | ||
|
|
f8af9c4fae | ||
|
|
9fa426d97f | ||
|
|
e3c95e376a | ||
|
|
00ed794c50 | ||
|
|
b52d5629e2 | ||
|
|
9d8e646d4e | ||
|
|
850c0b97e4 | ||
|
|
87035d0812 | ||
|
|
ef0168c9ff | ||
|
|
df1a5a25d4 | ||
|
|
c424884201 | ||
|
|
0a464a5223 | ||
|
|
8cd62bc132 | ||
|
|
123a74b38b | ||
|
|
dbeca806bb | ||
|
|
f790d3e3ed | ||
|
|
a12f7fc4d2 | ||
|
|
456ce38a10 | ||
|
|
72ef1668f2 | ||
|
|
fce8f52574 | ||
|
|
8fcfd7a308 | ||
|
|
04a41395c8 | ||
|
|
18e8201167 | ||
|
|
27b8794d8c | ||
|
|
3cb0df579c |
7
Makefile
7
Makefile
@@ -44,12 +44,8 @@ deploy-appbundle:
|
||||
cp \
|
||||
$(BUILD_DIR)/app.bundle.min.js \
|
||||
$(BUILD_DIR)/app.bundle.min.js.map \
|
||||
$(BUILD_DIR)/do_external_connect.min.js \
|
||||
$(BUILD_DIR)/do_external_connect.min.js.map \
|
||||
$(BUILD_DIR)/external_api.min.js \
|
||||
$(BUILD_DIR)/external_api.min.js.map \
|
||||
$(BUILD_DIR)/dial_in_info_bundle.min.js \
|
||||
$(BUILD_DIR)/dial_in_info_bundle.min.js.map \
|
||||
$(BUILD_DIR)/alwaysontop.min.js \
|
||||
$(BUILD_DIR)/alwaysontop.min.js.map \
|
||||
$(OUTPUT_DIR)/analytics-ga.js \
|
||||
@@ -70,7 +66,6 @@ deploy-lib-jitsi-meet:
|
||||
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.min.js \
|
||||
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.min.map \
|
||||
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.e2ee-worker.js \
|
||||
$(LIBJITSIMEET_DIR)/connection_optimization/external_connect.js \
|
||||
$(LIBJITSIMEET_DIR)/modules/browser/capabilities.json \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
@@ -131,7 +126,7 @@ dev: deploy-init deploy-css deploy-rnnoise-binary deploy-tflite deploy-meet-mode
|
||||
|
||||
source-package:
|
||||
mkdir -p source_package/jitsi-meet/css && \
|
||||
cp -r *.js *.html resources/*.txt connection_optimization favicon.ico fonts images libs static sounds LICENSE lang source_package/jitsi-meet && \
|
||||
cp -r *.js *.html resources/*.txt favicon.ico fonts images libs static sounds LICENSE lang source_package/jitsi-meet && \
|
||||
cp css/all.css source_package/jitsi-meet/css && \
|
||||
(cd source_package ; tar cjf ../jitsi-meet.tar.bz2 jitsi-meet) && \
|
||||
rm -rf source_package
|
||||
|
||||
11
app.js
11
app.js
@@ -33,17 +33,6 @@ window.APP = {
|
||||
API,
|
||||
conference,
|
||||
|
||||
// Used by do_external_connect.js if we receive the attach data after
|
||||
// connect was already executed. status property can be 'initialized',
|
||||
// 'ready', or 'connecting'. We are interested in 'ready' status only which
|
||||
// means that connect was executed but we have to wait for the attach data.
|
||||
// In status 'ready' handler property will be set to a function that will
|
||||
// finish the connect process when the attach data or error is received.
|
||||
connect: {
|
||||
handler: null,
|
||||
status: 'initialized'
|
||||
},
|
||||
|
||||
// Used for automated performance tests.
|
||||
connectionTimes: {
|
||||
'index.loaded': window.indexLoadedTime
|
||||
|
||||
@@ -141,7 +141,7 @@ import {
|
||||
showNotification,
|
||||
showWarningNotification
|
||||
} from './react/features/notifications';
|
||||
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
|
||||
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay/actions';
|
||||
import { suspendDetected } from './react/features/power-monitor';
|
||||
import { initPrejoin, makePrecallTest, setJoiningInProgress } from './react/features/prejoin/actions';
|
||||
import { isPrejoinPageVisible } from './react/features/prejoin/functions';
|
||||
|
||||
@@ -1146,6 +1146,13 @@ var config = {
|
||||
// }
|
||||
// },
|
||||
|
||||
// // The terms, privacy and help centre URL's.
|
||||
// legalUrls: {
|
||||
// helpCentre: 'https://web-cdn.jitsi.net/faq/meet-faq.html',
|
||||
// privacy: 'https://jitsi.org/meet/privacy',
|
||||
// terms: 'https://jitsi.org/meet/terms'
|
||||
// },
|
||||
|
||||
// A property to disable the right click context menu for localVideo
|
||||
// the menu has option to flip the locally seen video for local presentations
|
||||
// disableLocalVideoFlip: false,
|
||||
@@ -1365,7 +1372,6 @@ var config = {
|
||||
dialOutRegionUrl
|
||||
disableRemoteControl
|
||||
displayJids
|
||||
externalConnectUrl
|
||||
e2eeLabels
|
||||
firefox_fake_device
|
||||
googleApiApplicationClientID
|
||||
|
||||
@@ -32,54 +32,6 @@ const logger = Logger.getLogger(__filename);
|
||||
*/
|
||||
export const DISCO_JIBRI_FEATURE = 'http://jitsi.org/protocol/jibri';
|
||||
|
||||
/**
|
||||
* Checks if we have data to use attach instead of connect. If we have the data
|
||||
* executes attach otherwise check if we have to wait for the data. If we have
|
||||
* to wait for the attach data we are setting handler to APP.connect.handler
|
||||
* which is going to be called when the attach data is received otherwise
|
||||
* executes connect.
|
||||
*
|
||||
* @param {string} [id] user id
|
||||
* @param {string} [password] password
|
||||
* @param {string} [roomName] the name of the conference.
|
||||
*/
|
||||
function checkForAttachParametersAndConnect(id, password, connection) {
|
||||
if (window.XMPPAttachInfo) {
|
||||
APP.connect.status = 'connecting';
|
||||
|
||||
// When connection optimization is not deployed or enabled the default
|
||||
// value will be window.XMPPAttachInfo.status = "error"
|
||||
// If the connection optimization is deployed and enabled and there is
|
||||
// a failure the value will be window.XMPPAttachInfo.status = "error"
|
||||
if (window.XMPPAttachInfo.status === 'error') {
|
||||
connection.connect({
|
||||
id,
|
||||
password
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const attachOptions = window.XMPPAttachInfo.data;
|
||||
|
||||
if (attachOptions) {
|
||||
connection.attach(attachOptions);
|
||||
delete window.XMPPAttachInfo.data;
|
||||
} else {
|
||||
connection.connect({
|
||||
id,
|
||||
password
|
||||
});
|
||||
}
|
||||
} else {
|
||||
APP.connect.status = 'ready';
|
||||
APP.connect.handler
|
||||
= checkForAttachParametersAndConnect.bind(
|
||||
null,
|
||||
id, password, connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to open connection using provided credentials.
|
||||
* @param {string} [id]
|
||||
@@ -182,7 +134,10 @@ export async function connect(id, password) {
|
||||
APP.store.dispatch(setPrejoinDisplayNameRequired());
|
||||
}
|
||||
|
||||
checkForAttachParametersAndConnect(id, password, connection);
|
||||
connection.connect({
|
||||
id,
|
||||
password
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
'extends': '../react/.eslintrc.js'
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
/* global config, createConnectionExternally */
|
||||
|
||||
import getRoomName from '../react/features/base/config/getRoomName';
|
||||
import { parseURLParams } from '../react/features/base/util/parseURLParams';
|
||||
|
||||
/**
|
||||
* Implements external connect using createConnectionExternally function defined
|
||||
* in external_connect.js for Jitsi Meet. Parses the room name and JSON Web
|
||||
* Token (JWT) from the URL and executes createConnectionExternally.
|
||||
*
|
||||
* NOTE: If you are using lib-jitsi-meet without Jitsi Meet, you should use this
|
||||
* file as reference only because the implementation is Jitsi Meet-specific.
|
||||
*
|
||||
* NOTE: For optimal results this file should be included right after
|
||||
* external_connect.js.
|
||||
*/
|
||||
|
||||
if (typeof createConnectionExternally === 'function') {
|
||||
// URL params have higher priority than config params.
|
||||
// Do not use external connect if websocket is enabled.
|
||||
let url
|
||||
= parseURLParams(window.location, true, 'hash')[
|
||||
'config.externalConnectUrl']
|
||||
|| config.websocket ? undefined : config.externalConnectUrl;
|
||||
const isRecorder
|
||||
= parseURLParams(window.location, true, 'hash')['config.iAmRecorder'];
|
||||
|
||||
let roomName;
|
||||
|
||||
if (url && (roomName = getRoomName()) && !isRecorder) {
|
||||
url += `?room=${roomName}`;
|
||||
|
||||
const token = parseURLParams(window.location, true, 'search').jwt;
|
||||
|
||||
if (token) {
|
||||
url += `&token=${token}`;
|
||||
}
|
||||
|
||||
createConnectionExternally(
|
||||
url,
|
||||
connectionInfo => {
|
||||
// Sets that global variable to be used later by connect method
|
||||
// in connection.js.
|
||||
window.XMPPAttachInfo = {
|
||||
status: 'success',
|
||||
data: connectionInfo
|
||||
};
|
||||
checkForConnectHandlerAndConnect();
|
||||
},
|
||||
errorCallback);
|
||||
} else {
|
||||
errorCallback();
|
||||
}
|
||||
} else {
|
||||
errorCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connect from connection.js was executed and executes the handler
|
||||
* that is going to finish the connect work.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkForConnectHandlerAndConnect() {
|
||||
window.APP
|
||||
&& window.APP.connect.status === 'ready'
|
||||
&& window.APP.connect.handler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a callback to be invoked if anything goes wrong.
|
||||
*
|
||||
* @param {Error} error - The specifics of what went wrong.
|
||||
* @returns {void}
|
||||
*/
|
||||
function errorCallback(error) {
|
||||
// The value of error is undefined if external connect is disabled.
|
||||
error && console.warn(error);
|
||||
|
||||
// Sets that global variable to be used later by connect method in
|
||||
// connection.js.
|
||||
window.XMPPAttachInfo = {
|
||||
status: 'error'
|
||||
};
|
||||
checkForConnectHandlerAndConnect();
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
&-content {
|
||||
position: relative;
|
||||
right: auto;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 4px;
|
||||
max-height: 456px;
|
||||
overflow: auto;
|
||||
width: 300px;
|
||||
|
||||
@@ -201,11 +201,6 @@ $deepLinkingDialInConferenceIdPadding: inherit;
|
||||
$deepLinkingDialInConferenceIdBackgroundColor: inherit;
|
||||
$deepLinkingDialInConferenceIdBorderRadius: inherit;
|
||||
|
||||
$deepLinkingDialInConferenceNameFontSize: inherit;
|
||||
$deepLinkingDialInConferenceNameLineHeight: inherit;
|
||||
$deepLinkingDialInConferenceNameMarginBottom: none;
|
||||
$deepLinkingDialInConferenceNameFontWeight: inherit;
|
||||
|
||||
$deepLinkingDialInConferenceDescriptionFontSize: 0.8em;
|
||||
$deepLinkingDialInConferenceDescriptionLineHeight: inherit;
|
||||
$deepLinkingDialInConferenceDescriptionMarginBottom: none;
|
||||
|
||||
@@ -3,49 +3,38 @@
|
||||
display: inline-block;
|
||||
|
||||
&-container {
|
||||
max-height: 344px;
|
||||
background: $menuBG;
|
||||
border-radius: 3px;
|
||||
max-height: 456px;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
&-entry {
|
||||
cursor: pointer;
|
||||
height: 168px;
|
||||
margin-bottom: 8px;
|
||||
height: 138px;
|
||||
width: 244px;
|
||||
position: relative;
|
||||
width: 284px;
|
||||
margin: 0 7px 4px;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
border: 3px solid #31B76A;
|
||||
border-radius: 3px;
|
||||
cursor: default;
|
||||
height: 162px;
|
||||
width: 278px;
|
||||
border: 2px solid #4687ED;
|
||||
}
|
||||
}
|
||||
|
||||
&-video {
|
||||
border-radius: 3px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-overlay {
|
||||
background: rgba(42, 58, 75, 0.6);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&-error {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -56,23 +45,22 @@
|
||||
}
|
||||
|
||||
&-label {
|
||||
bottom: 8px;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: 100%;
|
||||
padding: 8px;
|
||||
z-index: 2;
|
||||
|
||||
&-container {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
background-color: #131519;
|
||||
border-radius: 3px;
|
||||
padding: 2px 8px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin: 0 auto;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
max-width: calc(100% - 16px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -80,8 +68,8 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div:nth-child(2) {
|
||||
padding: 0;
|
||||
|
||||
&-checkbox-container {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,13 @@
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
|
||||
.dial-in-conference-id {
|
||||
text-align: center;
|
||||
min-width: 200px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.dial-in-conference-id {
|
||||
margin: $deepLinkingDialInConferenceIdMargin;
|
||||
padding: $deepLinkingDialInConferenceIdPadding;
|
||||
@@ -74,24 +81,12 @@
|
||||
border-radius: $deepLinkingDialInConferenceIdBorderRadius;
|
||||
}
|
||||
|
||||
.dial-in-conference-name {
|
||||
font-size: $deepLinkingDialInConferenceNameFontSize;
|
||||
line-height: $deepLinkingDialInConferenceNameLineHeight;
|
||||
margin-bottom: $deepLinkingDialInConferenceNameMarginBottom;
|
||||
font-weight: $deepLinkingDialInConferenceNameFontWeight;
|
||||
}
|
||||
|
||||
.dial-in-conference-description {
|
||||
font-size: $deepLinkingDialInConferenceDescriptionFontSize;
|
||||
line-height: $deepLinkingDialInConferenceDescriptionLineHeight;
|
||||
margin-bottom: $deepLinkingDialInConferenceDescriptionMarginBottom;
|
||||
}
|
||||
|
||||
.dial-in-conference-pin {
|
||||
font-size: $deepLinkingDialInConferencePinFontSize;
|
||||
line-height: $deepLinkingDialInConferencePinLineHeight;
|
||||
}
|
||||
|
||||
.toll-free-list {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
@@ -63,3 +63,8 @@
|
||||
.desktop-source-preview-image-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.desktop-picker-tabs-container {
|
||||
width: 65%;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
}
|
||||
|
||||
.dial-in-numbers-list {
|
||||
max-width: 334px;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
@@ -59,10 +61,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #d1dbe8;
|
||||
}
|
||||
|
||||
.flag-cell {
|
||||
vertical-align: top;
|
||||
width: 30px;
|
||||
@@ -91,6 +89,7 @@
|
||||
font-weight: bold;
|
||||
list-style: none;
|
||||
vertical-align: top;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
li.toll-free:empty:before {
|
||||
@@ -119,11 +118,6 @@
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.dial-in-conference-name,
|
||||
.dial-in-conference-pin {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.dial-in-conference-description {
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
|
||||
.prejoin-input {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
|
||||
& input {
|
||||
text-align: center;
|
||||
|
||||
1
debian/jitsi-meet-web.install
vendored
1
debian/jitsi-meet-web.install
vendored
@@ -8,7 +8,6 @@ sounds /usr/share/jitsi-meet/
|
||||
fonts /usr/share/jitsi-meet/
|
||||
images /usr/share/jitsi-meet/
|
||||
lang /usr/share/jitsi-meet/
|
||||
connection_optimization /usr/share/jitsi-meet/
|
||||
resources/robots.txt /usr/share/jitsi-meet/
|
||||
resources/*.sh /usr/share/jitsi-meet/scripts/
|
||||
pwa-worker.js /usr/share/jitsi-meet/
|
||||
|
||||
@@ -93,7 +93,7 @@ server {
|
||||
}
|
||||
|
||||
# ensure all static content can always be found first
|
||||
location ~ ^/(libs|css|static|images|fonts|lang|sounds|connection_optimization|.well-known)/(.*)$
|
||||
location ~ ^/(libs|css|static|images|fonts|lang|sounds|.well-known)/(.*)$
|
||||
{
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
alias /usr/share/jitsi-meet/$1/$2;
|
||||
|
||||
BIN
images/logo-deep-linking-mobile.png
Normal file
BIN
images/logo-deep-linking-mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -182,8 +182,6 @@
|
||||
'error', loadErrHandler, true /* capture phase type of listener */);
|
||||
</script>
|
||||
<script><!--#include virtual="/config.js" --></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||
<!--#include virtual="connection_optimization/connection_optimization.html" -->
|
||||
<script src="libs/do_external_connect.min.js?v=1"></script>
|
||||
<script><!--#include virtual="/interface_config.js" --></script>
|
||||
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
|
||||
<script src="libs/app.bundle.min.js?v=139"></script>
|
||||
|
||||
@@ -389,7 +389,7 @@ PODS:
|
||||
- react-native-video/Video (6.0.0-alpha.1):
|
||||
- PromisesSwift
|
||||
- React-Core
|
||||
- react-native-webrtc (106.0.5):
|
||||
- react-native-webrtc (106.0.6):
|
||||
- JitsiWebRTC (~> 106.0.0)
|
||||
- React-Core
|
||||
- react-native-webview (11.15.1):
|
||||
@@ -754,7 +754,7 @@ SPEC CHECKSUMS:
|
||||
react-native-slider: 6e9b86e76cce4b9e35b3403193a6432ed07e0c81
|
||||
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
||||
react-native-video: bb6f12a7198db53b261fefb5d609dc77417acc8b
|
||||
react-native-webrtc: ef315d8adb68e78298b22100377d12ef168efdb5
|
||||
react-native-webrtc: 22ac6c64a1e38552bb173dde81ffea6979a58ef3
|
||||
react-native-webview: ea4899a1056c782afa96dd082179a66cbebf5504
|
||||
React-perflogger: 0458a87ea9a7342079e7a31b0d32b3734fb8415f
|
||||
React-RCTActionSheet: 22538001ea2926dea001111dd2846c13a0730bc9
|
||||
|
||||
@@ -916,6 +916,7 @@
|
||||
"localRecordingVideoWarning": "Um Ihr eigenes Kamerabild aufzuzeichnen, müssen Sie Ihre Kamera beim Start der Aufnahme einschalten",
|
||||
"localRecordingWarning": "Bitte prüfen Sie, dass das aktuelle Tab auswählen, um Bild und Ton aufzuzeichnen. Die Länge der Aufzeichnung ist aktuell auf 1GB beschränkt, was ungefähr 100 Minuten entspricht.",
|
||||
"loggedIn": "Als {{userName}} angemeldet",
|
||||
"noMicPermission": "Zugriff auf Mikrofon fehlgeschlagen. Bitte erlauben Sie den Zugriff auf das Mikrofon.",
|
||||
"noStreams": "Kein Ton oder Video erkannt.",
|
||||
"off": "Aufnahme gestoppt",
|
||||
"offBy": "{{name}} stoppte die Aufnahme",
|
||||
@@ -1285,6 +1286,7 @@
|
||||
"grantModerator": "Moderationsrechte vergeben",
|
||||
"hideSelfView": "Eigene Ansicht ausblenden",
|
||||
"kick": "Hinauswerfen",
|
||||
"mirrorVideo": "Mein Video spiegeln",
|
||||
"moderator": "Moderation",
|
||||
"mute": "Person ist stumm geschaltet",
|
||||
"muted": "Stummgeschaltet",
|
||||
|
||||
@@ -184,13 +184,21 @@
|
||||
"deepLinking": {
|
||||
"appNotInstalled": "You need the {{app}} mobile app to join this meeting on your phone.",
|
||||
"description": "Nothing happened? We tried launching your meeting in the {{app}} desktop app. Try again or launch it in the {{app}} web app.",
|
||||
"descriptionNew": "Nothing happened? We tried launching your meeting in the {{app}} desktop app. <br /><br /> You can try again or launch it on web.",
|
||||
"descriptionWithoutWeb": "Nothing happened? We tried launching your meeting in the {{app}} desktop app.",
|
||||
"downloadApp": "Download the app",
|
||||
"downloadMobileApp": "Download from App Store",
|
||||
"ifDoNotHaveApp": "If you don't have the app yet:",
|
||||
"ifHaveApp": "If you already have the app:",
|
||||
"joinInApp": "Join this meeting using the app",
|
||||
"joinInAppNew": "Join in app",
|
||||
"joinInBrowser": "Join in browser",
|
||||
"launchMeetingLabel": "How do you want to join this meeting?",
|
||||
"launchWebButton": "Launch in web",
|
||||
"noMobileApp": "You don’t have the app?",
|
||||
"termsAndConditions": "By continuing you agree to our <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>terms & conditions.</a>",
|
||||
"title": "Launching your meeting in {{app}}...",
|
||||
"titleNew": "Launching your meeting ...",
|
||||
"tryAgainButton": "Try again in desktop",
|
||||
"unsupportedBrowser": "It looks like you're using a browser we don't support."
|
||||
},
|
||||
@@ -1286,6 +1294,7 @@
|
||||
"grantModerator": "Grant Moderator Rights",
|
||||
"hideSelfView": "Hide self view",
|
||||
"kick": "Kick out",
|
||||
"mirrorVideo": "Mirror my video",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Participant is muted",
|
||||
"muted": "Muted",
|
||||
|
||||
@@ -71,8 +71,13 @@ import {
|
||||
import { appendSuffix } from '../../react/features/display-name';
|
||||
import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox';
|
||||
import { setMediaEncryptionKey, toggleE2EE } from '../../react/features/e2ee/actions';
|
||||
import { addStageParticipant, resizeFilmStrip, setVolume } from '../../react/features/filmstrip/actions.web';
|
||||
import { isStageFilmstripAvailable } from '../../react/features/filmstrip/functions.web';
|
||||
import {
|
||||
addStageParticipant,
|
||||
resizeFilmStrip,
|
||||
setVolume,
|
||||
togglePinStageParticipant
|
||||
} from '../../react/features/filmstrip/actions.web';
|
||||
import { getPinnedActiveParticipants, isStageFilmstripAvailable } from '../../react/features/filmstrip/functions.web';
|
||||
import { invite } from '../../react/features/invite';
|
||||
import {
|
||||
selectParticipantInLargeVideo
|
||||
@@ -241,6 +246,22 @@ function initCommands() {
|
||||
logger.debug('Pin participant command received');
|
||||
|
||||
const state = APP.store.getState();
|
||||
|
||||
// if id not provided, unpin everybody.
|
||||
if (!id) {
|
||||
if (isStageFilmstripAvailable(state)) {
|
||||
const pinnedParticipants = getPinnedActiveParticipants(state);
|
||||
|
||||
pinnedParticipants?.forEach(p => {
|
||||
APP.store.dispatch(togglePinStageParticipant(p.participantId));
|
||||
});
|
||||
} else {
|
||||
APP.store.dispatch(pinParticipant());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const participant = videoType === VIDEO_TYPE.DESKTOP
|
||||
? getVirtualScreenshareParticipantByOwnerId(state, id) : getParticipantById(state, id);
|
||||
|
||||
@@ -254,7 +275,7 @@ function initCommands() {
|
||||
|
||||
const participantId = participant.id;
|
||||
|
||||
if (isStageFilmstripAvailable(APP.store.getState())) {
|
||||
if (isStageFilmstripAvailable(state)) {
|
||||
APP.store.dispatch(addStageParticipant(participantId, true));
|
||||
} else {
|
||||
APP.store.dispatch(pinParticipant(participantId));
|
||||
|
||||
396
package-lock.json
generated
396
package-lock.json
generated
@@ -15,10 +15,8 @@
|
||||
"@atlaskit/icon": "21.2.0",
|
||||
"@atlaskit/inline-dialog": "13.0.9",
|
||||
"@atlaskit/inline-message": "11.0.8",
|
||||
"@atlaskit/modal-dialog": "11.2.4",
|
||||
"@atlaskit/multi-select": "15.0.5",
|
||||
"@atlaskit/spinner": "15.0.6",
|
||||
"@atlaskit/tabs": "12.1.2",
|
||||
"@atlaskit/theme": "11.0.2",
|
||||
"@atlaskit/tooltip": "17.1.2",
|
||||
"@emotion/react": "11.10.0",
|
||||
@@ -73,7 +71,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/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -112,7 +110,7 @@
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
|
||||
"react-native-watch-connectivity": "1.0.11",
|
||||
"react-native-webrtc": "106.0.5",
|
||||
"react-native-webrtc": "106.0.6",
|
||||
"react-native-webview": "11.15.1",
|
||||
"react-native-youtube-iframe": "2.2.1",
|
||||
"react-redux": "7.1.0",
|
||||
@@ -140,7 +138,6 @@
|
||||
"@babel/preset-env": "7.16.0",
|
||||
"@babel/preset-flow": "7.16.0",
|
||||
"@babel/preset-react": "7.16.0",
|
||||
"@babel/runtime": "7.16.0",
|
||||
"@jitsi/eslint-config": "4.1.5",
|
||||
"@types/js-md5": "0.4.3",
|
||||
"@types/lodash": "4.14.182",
|
||||
@@ -167,13 +164,11 @@
|
||||
"eslint-plugin-react": "7.26.1",
|
||||
"eslint-plugin-react-native": "3.11.0",
|
||||
"eslint-plugin-typescript-sort-keys": "2.1.0",
|
||||
"imports-loader": "0.7.1",
|
||||
"jetifier": "1.6.4",
|
||||
"metro-react-native-babel-preset": "0.67.0",
|
||||
"patch-package": "6.4.7",
|
||||
"process": "0.11.10",
|
||||
"sass": "1.26.8",
|
||||
"string-replace-loader": "3.0.3",
|
||||
"style-loader": "3.3.1",
|
||||
"traverse": "0.6.6",
|
||||
"ts-loader": "9.4.1",
|
||||
@@ -336,44 +331,6 @@
|
||||
"react": "^16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/blanket": {
|
||||
"version": "11.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/blanket/-/blanket-11.4.1.tgz",
|
||||
"integrity": "sha512-jQor3MUcsD04/lUjdW89daYw6t0gFEuWzyqYGKgDQpVeQ3n0Lfrg5S3Eogc2cbRzgKMGaET6HMB+FD6EpE2QsQ==",
|
||||
"dependencies": {
|
||||
"@atlaskit/analytics-next": "^8.0.0",
|
||||
"@atlaskit/ds-lib": "^1.2.0",
|
||||
"@atlaskit/theme": "^11.2.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@emotion/core": "^10.0.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/blanket/node_modules/@atlaskit/theme": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-11.5.2.tgz",
|
||||
"integrity": "sha512-iycVcMzGaPboTVu0s+DoeV3oZZGIxXca1IjVtlxERA/78dftYoVTjiBnh6XtAQnIgB0O+SaAOQaaHJZgJ4gwpQ==",
|
||||
"dependencies": {
|
||||
"@atlaskit/tokens": "^0.2.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"exenv": "^1.2.2",
|
||||
"prop-types": "^15.5.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0",
|
||||
"styled-components": "^3.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/blanket/node_modules/@atlaskit/tokens": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tokens/-/tokens-0.2.1.tgz",
|
||||
"integrity": "sha512-Fm/F9mOC7QI5OSwvyY5N5+cvhx0lPLPAE5sE520ztb+YOaSiFxHZ13YBJkkdVTtxbGsYayYWITinGDQcCnhLog==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/button": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-15.1.4.tgz",
|
||||
@@ -663,93 +620,6 @@
|
||||
"styled-components": "^3.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/modal-dialog": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/modal-dialog/-/modal-dialog-11.2.4.tgz",
|
||||
"integrity": "sha512-+Qe2Ai2qiBCaLm5F144Kn+M8931097mlZTbBppCrhBbanOyZJHrRkzqbCCd5NGGLpfLUZxfZaQYJssrHfGCDnA==",
|
||||
"dependencies": {
|
||||
"@atlaskit/analytics-next": "^8.0.0",
|
||||
"@atlaskit/blanket": "^11.0.0",
|
||||
"@atlaskit/button": "^15.1.0",
|
||||
"@atlaskit/icon": "^21.1.0",
|
||||
"@atlaskit/portal": "^4.0.0",
|
||||
"@atlaskit/theme": "^11.0.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@emotion/core": "^10.0.9",
|
||||
"@emotion/styled": "^10.0.7",
|
||||
"exenv": "^1.2.2",
|
||||
"raf-schd": "^2.1.0",
|
||||
"react-focus-lock": "^1.19.1",
|
||||
"react-scrolllock": "^5.0.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-uid": "^2.2.0",
|
||||
"tiny-invariant": "^0.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/modal-dialog/node_modules/@emotion/styled": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz",
|
||||
"integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==",
|
||||
"dependencies": {
|
||||
"@emotion/styled-base": "^10.3.0",
|
||||
"babel-plugin-emotion": "^10.0.27"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/core": "^10.0.27",
|
||||
"react": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/modal-dialog/node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/modal-dialog/node_modules/focus-lock": {
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.6.8.tgz",
|
||||
"integrity": "sha512-vkHTluRCoq9FcsrldC0ulQHiyBYgVJB2CX53I8r0nTC6KnEij7Of0jpBspjt3/CuNb6fyoj3aOh9J2HgQUM0og=="
|
||||
},
|
||||
"node_modules/@atlaskit/modal-dialog/node_modules/raf-schd": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-2.1.2.tgz",
|
||||
"integrity": "sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g=="
|
||||
},
|
||||
"node_modules/@atlaskit/modal-dialog/node_modules/react-focus-lock": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-1.19.1.tgz",
|
||||
"integrity": "sha512-TPpfiack1/nF4uttySfpxPk4rGZTLXlaZl7ncZg/ELAk24Iq2B1UUaUioID8H8dneUXqznT83JTNDHDj+kwryw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"focus-lock": "^0.6.3",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-clientside-effect": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.0.0 || ^16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/modal-dialog/node_modules/react-transition-group": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
||||
"integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/motion": {
|
||||
"version": "0.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/motion/-/motion-0.4.8.tgz",
|
||||
@@ -845,20 +715,6 @@
|
||||
"react": "^16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/tabs": {
|
||||
"version": "12.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tabs/-/tabs-12.1.2.tgz",
|
||||
"integrity": "sha512-/PbNIwXh0YMoo8pUjLwJfZEe+vtHENhBWuQc1pK4rW7JNBAD0VYrAJ+JLiZ+ZWGFCOoCSneoJguDBH0I2FMrqA==",
|
||||
"dependencies": {
|
||||
"@atlaskit/analytics-next": "^8.0.0",
|
||||
"@atlaskit/theme": "^11.0.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@emotion/core": "^10.0.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/tag": {
|
||||
"version": "11.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tag/-/tag-11.2.5.tgz",
|
||||
@@ -12088,16 +11944,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/imports-loader": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/imports-loader/-/imports-loader-0.7.1.tgz",
|
||||
"integrity": "sha1-8gS180cCoywdt9SNidXoZ6BEElM=",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loader-utils": "^1.0.2",
|
||||
"source-map": "^0.5.6"
|
||||
}
|
||||
},
|
||||
"node_modules/imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
@@ -13553,8 +13399,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-AAEClrQNOVHNO1lKr/F1SOyiduZfI6bql3eiIxC3LZ5cBcyoRVmwI6uiAbLail0VkuKnTecEWcfYZ9lvokxrMw==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-g7JVvBfZixl1fKZI4ZMm3nvMasEz5sdapMzZdc76kA/eZSej2QuNK+W9cB8IypB7dqeTM4yzbfzi9rDipyWn+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
@@ -16545,9 +16391,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webrtc": {
|
||||
"version": "106.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.5.tgz",
|
||||
"integrity": "sha512-EINzYpTZh6zXb2lcGH13Ieli1ur3M1FaT8R8WMqfUZEW8/y0WV6yBeQQVz55OA4LtWnBUX0RZyaYQ4aZN4e1Sw==",
|
||||
"version": "106.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.6.tgz",
|
||||
"integrity": "sha512-mxRqR/sNZfVnbTM8cd90Y+A23H53jvZ/j0W7MSSFAbsQrj9Jdew1+7tJVTcBieF1S4ytTLI/R95scOQ4+qeE2Q==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"adm-zip": "0.5.9",
|
||||
@@ -18207,51 +18053,6 @@
|
||||
"integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/string-replace-loader": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string-replace-loader/-/string-replace-loader-3.0.3.tgz",
|
||||
"integrity": "sha512-8c26Dl6H9XmKNj3mFBvaUYR7ImOxQ4YRBFuUju78wXpa1cDpyDYvKmqGg8mfkxdYexQ/BBogB7PELlLnmR08nw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/string-replace-loader/node_modules/loader-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-replace-loader/node_modules/schema-utils": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/string-replace-to-array": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz",
|
||||
@@ -18787,11 +18588,6 @@
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-0.0.3.tgz",
|
||||
"integrity": "sha512-SA2YwvDrCITM9fTvHTHRpq9W6L2fBsClbqm3maT5PZux4Z73SPPDYwJMtnoWh6WMgmCkJij/LaOlWiqJqFMK8g=="
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
@@ -20729,39 +20525,6 @@
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@atlaskit/blanket": {
|
||||
"version": "11.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/blanket/-/blanket-11.4.1.tgz",
|
||||
"integrity": "sha512-jQor3MUcsD04/lUjdW89daYw6t0gFEuWzyqYGKgDQpVeQ3n0Lfrg5S3Eogc2cbRzgKMGaET6HMB+FD6EpE2QsQ==",
|
||||
"requires": {
|
||||
"@atlaskit/analytics-next": "^8.0.0",
|
||||
"@atlaskit/ds-lib": "^1.2.0",
|
||||
"@atlaskit/theme": "^11.2.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@emotion/core": "^10.0.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/theme": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-11.5.2.tgz",
|
||||
"integrity": "sha512-iycVcMzGaPboTVu0s+DoeV3oZZGIxXca1IjVtlxERA/78dftYoVTjiBnh6XtAQnIgB0O+SaAOQaaHJZgJ4gwpQ==",
|
||||
"requires": {
|
||||
"@atlaskit/tokens": "^0.2.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"exenv": "^1.2.2",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"@atlaskit/tokens": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tokens/-/tokens-0.2.1.tgz",
|
||||
"integrity": "sha512-Fm/F9mOC7QI5OSwvyY5N5+cvhx0lPLPAE5sE520ztb+YOaSiFxHZ13YBJkkdVTtxbGsYayYWITinGDQcCnhLog==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@atlaskit/button": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-15.1.4.tgz",
|
||||
@@ -20990,81 +20753,6 @@
|
||||
"react-scrolllock": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"@atlaskit/modal-dialog": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/modal-dialog/-/modal-dialog-11.2.4.tgz",
|
||||
"integrity": "sha512-+Qe2Ai2qiBCaLm5F144Kn+M8931097mlZTbBppCrhBbanOyZJHrRkzqbCCd5NGGLpfLUZxfZaQYJssrHfGCDnA==",
|
||||
"requires": {
|
||||
"@atlaskit/analytics-next": "^8.0.0",
|
||||
"@atlaskit/blanket": "^11.0.0",
|
||||
"@atlaskit/button": "^15.1.0",
|
||||
"@atlaskit/icon": "^21.1.0",
|
||||
"@atlaskit/portal": "^4.0.0",
|
||||
"@atlaskit/theme": "^11.0.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@emotion/core": "^10.0.9",
|
||||
"@emotion/styled": "^10.0.7",
|
||||
"exenv": "^1.2.2",
|
||||
"raf-schd": "^2.1.0",
|
||||
"react-focus-lock": "^1.19.1",
|
||||
"react-scrolllock": "^5.0.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-uid": "^2.2.0",
|
||||
"tiny-invariant": "^0.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/styled": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz",
|
||||
"integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==",
|
||||
"requires": {
|
||||
"@emotion/styled-base": "^10.3.0",
|
||||
"babel-plugin-emotion": "^10.0.27"
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"focus-lock": {
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.6.8.tgz",
|
||||
"integrity": "sha512-vkHTluRCoq9FcsrldC0ulQHiyBYgVJB2CX53I8r0nTC6KnEij7Of0jpBspjt3/CuNb6fyoj3aOh9J2HgQUM0og=="
|
||||
},
|
||||
"raf-schd": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-2.1.2.tgz",
|
||||
"integrity": "sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g=="
|
||||
},
|
||||
"react-focus-lock": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-1.19.1.tgz",
|
||||
"integrity": "sha512-TPpfiack1/nF4uttySfpxPk4rGZTLXlaZl7ncZg/ELAk24Iq2B1UUaUioID8H8dneUXqznT83JTNDHDj+kwryw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"focus-lock": "^0.6.3",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-clientside-effect": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
||||
"integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@atlaskit/motion": {
|
||||
"version": "0.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/motion/-/motion-0.4.8.tgz",
|
||||
@@ -21139,17 +20827,6 @@
|
||||
"@emotion/core": "^10.0.9"
|
||||
}
|
||||
},
|
||||
"@atlaskit/tabs": {
|
||||
"version": "12.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tabs/-/tabs-12.1.2.tgz",
|
||||
"integrity": "sha512-/PbNIwXh0YMoo8pUjLwJfZEe+vtHENhBWuQc1pK4rW7JNBAD0VYrAJ+JLiZ+ZWGFCOoCSneoJguDBH0I2FMrqA==",
|
||||
"requires": {
|
||||
"@atlaskit/analytics-next": "^8.0.0",
|
||||
"@atlaskit/theme": "^11.0.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@emotion/core": "^10.0.9"
|
||||
}
|
||||
},
|
||||
"@atlaskit/tag": {
|
||||
"version": "11.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tag/-/tag-11.2.5.tgz",
|
||||
@@ -29506,16 +29183,6 @@
|
||||
"resolve-cwd": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"imports-loader": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/imports-loader/-/imports-loader-0.7.1.tgz",
|
||||
"integrity": "sha1-8gS180cCoywdt9SNidXoZ6BEElM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^1.0.2",
|
||||
"source-map": "^0.5.6"
|
||||
}
|
||||
},
|
||||
"imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
@@ -30611,8 +30278,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-AAEClrQNOVHNO1lKr/F1SOyiduZfI6bql3eiIxC3LZ5cBcyoRVmwI6uiAbLail0VkuKnTecEWcfYZ9lvokxrMw==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-g7JVvBfZixl1fKZI4ZMm3nvMasEz5sdapMzZdc76kA/eZSej2QuNK+W9cB8IypB7dqeTM4yzbfzi9rDipyWn+w==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
@@ -32895,9 +32562,9 @@
|
||||
}
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "106.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.5.tgz",
|
||||
"integrity": "sha512-EINzYpTZh6zXb2lcGH13Ieli1ur3M1FaT8R8WMqfUZEW8/y0WV6yBeQQVz55OA4LtWnBUX0RZyaYQ4aZN4e1Sw==",
|
||||
"version": "106.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.6.tgz",
|
||||
"integrity": "sha512-mxRqR/sNZfVnbTM8cd90Y+A23H53jvZ/j0W7MSSFAbsQrj9Jdew1+7tJVTcBieF1S4ytTLI/R95scOQ4+qeE2Q==",
|
||||
"requires": {
|
||||
"adm-zip": "0.5.9",
|
||||
"base64-js": "1.5.1",
|
||||
@@ -34180,40 +33847,6 @@
|
||||
"integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==",
|
||||
"dev": true
|
||||
},
|
||||
"string-replace-loader": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string-replace-loader/-/string-replace-loader-3.0.3.tgz",
|
||||
"integrity": "sha512-8c26Dl6H9XmKNj3mFBvaUYR7ImOxQ4YRBFuUju78wXpa1cDpyDYvKmqGg8mfkxdYexQ/BBogB7PELlLnmR08nw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"loader-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"string-replace-to-array": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz",
|
||||
@@ -34609,11 +34242,6 @@
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"dev": true
|
||||
},
|
||||
"tiny-invariant": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-0.0.3.tgz",
|
||||
"integrity": "sha512-SA2YwvDrCITM9fTvHTHRpq9W6L2fBsClbqm3maT5PZux4Z73SPPDYwJMtnoWh6WMgmCkJij/LaOlWiqJqFMK8g=="
|
||||
},
|
||||
"tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
|
||||
@@ -20,10 +20,8 @@
|
||||
"@atlaskit/icon": "21.2.0",
|
||||
"@atlaskit/inline-dialog": "13.0.9",
|
||||
"@atlaskit/inline-message": "11.0.8",
|
||||
"@atlaskit/modal-dialog": "11.2.4",
|
||||
"@atlaskit/multi-select": "15.0.5",
|
||||
"@atlaskit/spinner": "15.0.6",
|
||||
"@atlaskit/tabs": "12.1.2",
|
||||
"@atlaskit/theme": "11.0.2",
|
||||
"@atlaskit/tooltip": "17.1.2",
|
||||
"@emotion/react": "11.10.0",
|
||||
@@ -78,7 +76,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/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1585.0.0+362d1b2c/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -117,7 +115,7 @@
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
|
||||
"react-native-watch-connectivity": "1.0.11",
|
||||
"react-native-webrtc": "106.0.5",
|
||||
"react-native-webrtc": "106.0.6",
|
||||
"react-native-webview": "11.15.1",
|
||||
"react-native-youtube-iframe": "2.2.1",
|
||||
"react-redux": "7.1.0",
|
||||
@@ -145,7 +143,6 @@
|
||||
"@babel/preset-env": "7.16.0",
|
||||
"@babel/preset-flow": "7.16.0",
|
||||
"@babel/preset-react": "7.16.0",
|
||||
"@babel/runtime": "7.16.0",
|
||||
"@jitsi/eslint-config": "4.1.5",
|
||||
"@types/js-md5": "0.4.3",
|
||||
"@types/lodash": "4.14.182",
|
||||
@@ -172,13 +169,11 @@
|
||||
"eslint-plugin-react": "7.26.1",
|
||||
"eslint-plugin-react-native": "3.11.0",
|
||||
"eslint-plugin-typescript-sort-keys": "2.1.0",
|
||||
"imports-loader": "0.7.1",
|
||||
"jetifier": "1.6.4",
|
||||
"metro-react-native-babel-preset": "0.67.0",
|
||||
"patch-package": "6.4.7",
|
||||
"process": "0.11.10",
|
||||
"sass": "1.26.8",
|
||||
"string-replace-loader": "3.0.3",
|
||||
"style-loader": "3.3.1",
|
||||
"traverse": "0.6.6",
|
||||
"ts-loader": "9.4.1",
|
||||
|
||||
@@ -30,12 +30,10 @@ import {
|
||||
// @ts-ignore
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
import { clearNotifications } from '../notifications/actions';
|
||||
// @ts-ignore
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
import { addTrackStateToURL, getDefaultURL } from './functions.native';
|
||||
import logger from './logger';
|
||||
import { IStore } from './types';
|
||||
import { IReloadNowOptions, IStore } from './types';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
@@ -46,9 +44,10 @@ export * from './actions.any';
|
||||
* @param {string|undefined} uri - The URI to which to navigate. It may be a
|
||||
* full URL with an HTTP(S) scheme, a full or partial URI with the app-specific
|
||||
* scheme, or a mere room name.
|
||||
* @param {Object} [options] - Options.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function appNavigate(uri?: string) {
|
||||
export function appNavigate(uri?: string, options: IReloadNowOptions = {}) {
|
||||
logger.info(`appNavigate to ${uri}`);
|
||||
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
@@ -144,7 +143,10 @@ export function appNavigate(uri?: string) {
|
||||
dispatch(createDesiredLocalTracks());
|
||||
dispatch(clearNotifications());
|
||||
|
||||
if (isPrejoinPageEnabled(getState())) {
|
||||
// @ts-ignore
|
||||
const { hidePrejoin } = options;
|
||||
|
||||
if (!hidePrejoin && isPrejoinPageEnabled(getState())) {
|
||||
navigateRoot(screen.preJoin);
|
||||
} else {
|
||||
dispatch(connect());
|
||||
@@ -177,7 +179,6 @@ export function maybeRedirectToWelcomePage(options: any) { // eslint-disable-lin
|
||||
*/
|
||||
export function reloadNow() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
dispatch(setFatalError(undefined));
|
||||
|
||||
const state = getState();
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
@@ -188,6 +189,8 @@ export function reloadNow() {
|
||||
|
||||
logger.info(`Reloading the conference using URL: ${locationURL}`);
|
||||
|
||||
dispatch(appNavigate(toURLString(newURL)));
|
||||
dispatch(appNavigate(toURLString(newURL), {
|
||||
hidePrejoin: true
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
import { clearNotifications, showNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import { setFatalError } from '../overlay/actions';
|
||||
import { isWelcomePageEnabled } from '../welcome/functions';
|
||||
|
||||
import {
|
||||
@@ -222,7 +221,6 @@ export function maybeRedirectToWelcomePage(options: { feedbackSubmitted?: boolea
|
||||
*/
|
||||
export function reloadNow() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
dispatch(setFatalError(undefined));
|
||||
|
||||
const state = getState();
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { BaseApp } from '../../base/app';
|
||||
import { toURLString } from '../../base/util';
|
||||
import { OverlayContainer } from '../../overlay';
|
||||
import { appNavigate } from '../actions';
|
||||
import { getDefaultURL } from '../functions';
|
||||
|
||||
@@ -73,23 +70,7 @@ export class AbstractApp extends BaseApp<Props, *> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an extra {@link ReactElement}s to be added (unconditionally)
|
||||
* alongside the main element.
|
||||
*
|
||||
* @abstract
|
||||
* @protected
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_createExtraElement() {
|
||||
return (
|
||||
<Fragment>
|
||||
<OverlayContainer />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
_createMainElement: (React$Element<*>, Object) => ?React$Element<*>;
|
||||
_createMainElement: (React.ReactElement, Object) => ?React.ReactElement;
|
||||
|
||||
/**
|
||||
* Gets the default URL to be opened when this {@code App} mounts.
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import GlobalStyles from '../../base/ui/components/GlobalStyles.web';
|
||||
import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider.web';
|
||||
import DialogContainer from '../../base/ui/components/web/DialogContainer';
|
||||
import { ChromeExtensionBanner } from '../../chrome-extension-banner';
|
||||
import OverlayContainer from '../../overlay/components/web/OverlayContainer';
|
||||
|
||||
import { AbstractApp } from './AbstractApp';
|
||||
|
||||
@@ -14,12 +13,30 @@ import { AbstractApp } from './AbstractApp';
|
||||
import '../middlewares';
|
||||
import '../reducers';
|
||||
|
||||
|
||||
/**
|
||||
* Root app {@code Component} on Web/React.
|
||||
*
|
||||
* @augments AbstractApp
|
||||
*/
|
||||
export class App extends AbstractApp {
|
||||
|
||||
/**
|
||||
* Creates an extra {@link ReactElement}s to be added (unconditionally)
|
||||
* alongside the main element.
|
||||
*
|
||||
* @abstract
|
||||
* @protected
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_createExtraElement() {
|
||||
return (
|
||||
<Fragment>
|
||||
<OverlayContainer />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
|
||||
* the top most component.
|
||||
|
||||
@@ -21,6 +21,5 @@ import '../toolbox/middleware';
|
||||
import '../face-landmarks/middleware';
|
||||
import '../gifs/middleware';
|
||||
import '../whiteboard/middleware';
|
||||
import '../base/dialog/middleware';
|
||||
|
||||
import './middlewares.any';
|
||||
|
||||
@@ -165,3 +165,7 @@ export interface IReduxState {
|
||||
'features/virtual-background': IVirtualBackground;
|
||||
'features/whiteboard': IWhiteboardState;
|
||||
}
|
||||
|
||||
export interface IReloadNowOptions {
|
||||
hidePrejoin?: boolean;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ function _upgradeRoleFinished(
|
||||
name: authenticationError || connectionError,
|
||||
...other
|
||||
};
|
||||
progress = authenticationError ? 0.5 : 0;
|
||||
progress = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -207,7 +207,7 @@ class LoginDialog extends Component<IProps, IState> {
|
||||
let messageKey;
|
||||
|
||||
if (progress && progress < 1) {
|
||||
messageKey = t('connection.FETCH_SESSION_ID');
|
||||
messageKey = 'connection.FETCH_SESSION_ID';
|
||||
} else if (error) {
|
||||
const { name } = error;
|
||||
|
||||
@@ -218,14 +218,14 @@ class LoginDialog extends Component<IProps, IState> {
|
||||
&& credentials.jid === toJid(username, configHosts ?? { authdomain: '',
|
||||
domain: '' })
|
||||
&& credentials.password === password) {
|
||||
messageKey = t('dialog.incorrectPassword');
|
||||
messageKey = 'dialog.incorrectPassword';
|
||||
}
|
||||
} else if (name) {
|
||||
messageKey = t('dialog.connectErrorWithMsg');
|
||||
messageKey = 'dialog.connectErrorWithMsg';
|
||||
messageOptions.msg = `${name} ${error.message}`;
|
||||
}
|
||||
} else if (connecting) {
|
||||
messageKey = t('connection.CONNECTING');
|
||||
messageKey = 'connection.CONNECTING';
|
||||
}
|
||||
|
||||
if (messageKey) {
|
||||
|
||||
@@ -388,6 +388,11 @@ export interface IConfig {
|
||||
lastNLimits?: {
|
||||
[key: number]: number;
|
||||
};
|
||||
legalUrls?: {
|
||||
helpCentre: string;
|
||||
privacy: string;
|
||||
terms: string;
|
||||
};
|
||||
liveStreaming?: {
|
||||
dataPrivacyLink?: string;
|
||||
enabled?: boolean;
|
||||
|
||||
@@ -70,3 +70,18 @@ export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-bac
|
||||
export const FEATURE_FLAGS = {
|
||||
SSRC_REWRITING: 'ssrcRewritingEnabled'
|
||||
};
|
||||
|
||||
/**
|
||||
* The URL at which the terms (of service/use) are available to the user.
|
||||
*/
|
||||
export const DEFAULT_TERMS_URL = 'https://jitsi.org/meet/terms';
|
||||
|
||||
/**
|
||||
* The URL at which the privacy policy is available to the user.
|
||||
*/
|
||||
export const DEFAULT_PRIVACY_URL = 'https://jitsi.org/meet/privacy';
|
||||
|
||||
/**
|
||||
* The URL at which the help centre is available to the user.
|
||||
*/
|
||||
export const DEFAULT_HELP_CENTRE_URL = 'https://web-cdn.jitsi.net/faq/meet-faq.html';
|
||||
|
||||
@@ -11,7 +11,13 @@ import { parseURLParams } from '../util/parseURLParams';
|
||||
|
||||
import { IConfig } from './configType';
|
||||
import CONFIG_WHITELIST from './configWhitelist';
|
||||
import { FEATURE_FLAGS, _CONFIG_STORE_PREFIX } from './constants';
|
||||
import {
|
||||
DEFAULT_HELP_CENTRE_URL,
|
||||
DEFAULT_PRIVACY_URL,
|
||||
DEFAULT_TERMS_URL,
|
||||
FEATURE_FLAGS,
|
||||
_CONFIG_STORE_PREFIX
|
||||
} from './constants';
|
||||
import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
|
||||
import logger from './logger';
|
||||
|
||||
@@ -326,3 +332,24 @@ export function getDialOutUrl(state: IReduxState) {
|
||||
export function getSecurityUiConfig(state: IReduxState) {
|
||||
return state['features/base/config']?.securityUi || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the terms, privacy and help centre URL's.
|
||||
*
|
||||
* @param {IReduxState} state - The state of the application.
|
||||
* @returns {{
|
||||
* privacy: string,
|
||||
* helpCentre: string,
|
||||
* terms: string
|
||||
* }}
|
||||
*/
|
||||
export function getLegalUrls(state: IReduxState) {
|
||||
const helpCentreURL = state['features/base/config']?.helpCentreURL;
|
||||
const configLegalUrls = state['features/base/config']?.legalUrls;
|
||||
|
||||
return {
|
||||
privacy: configLegalUrls?.privacy || DEFAULT_PRIVACY_URL,
|
||||
helpCentre: helpCentreURL || configLegalUrls?.helpCentre || DEFAULT_HELP_CENTRE_URL,
|
||||
terms: configLegalUrls?.terms || DEFAULT_TERMS_URL
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,9 +52,7 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
|
||||
const settings = state['features/base/settings'];
|
||||
const config: IConfig = {};
|
||||
|
||||
// FIXME: P2P is currently temporality disabled on mobile.
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (false && typeof settings.disableP2P !== 'undefined') {
|
||||
if (typeof settings.disableP2P !== 'undefined') {
|
||||
config.p2p = { enabled: !settings.disableP2P };
|
||||
}
|
||||
|
||||
|
||||
@@ -57,10 +57,8 @@ const INITIAL_RN_STATE: IConfig = {
|
||||
// than requiring this override here...
|
||||
|
||||
p2p: {
|
||||
// Temporarily disable P2P on mobile while we sort out some (codec?) issues.
|
||||
enabled: false,
|
||||
disabledCodec: 'vp9',
|
||||
preferredCodec: 'h264'
|
||||
preferredCodec: 'vp8'
|
||||
},
|
||||
|
||||
videoQuality: {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Container, Text } from '../../react';
|
||||
import { type StyleType } from '../../styles';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Children of the component.
|
||||
*/
|
||||
children: string | React$Node,
|
||||
|
||||
style: ?StyleType
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic dialog content container to provide the same styling for all custom
|
||||
* dialogs.
|
||||
*/
|
||||
export default class DialogContent extends Component<Props> {
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { children, style } = this.props;
|
||||
|
||||
const childrenComponent = typeof children === 'string'
|
||||
? <Text style = { style }>{ children }</Text>
|
||||
: children;
|
||||
|
||||
return (
|
||||
<Container style = { styles.dialogContainer }>
|
||||
{ childrenComponent }
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './_';
|
||||
|
||||
export { default as DialogContent } from './DialogContent';
|
||||
|
||||
@@ -48,7 +48,7 @@ type Props = {
|
||||
/**
|
||||
* Dialog title.
|
||||
*/
|
||||
title?: string,
|
||||
title?: string
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
// @ts-ignore
|
||||
import { randomInt } from '@jitsi/js-utils/random';
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { appNavigate, reloadNow } from '../../../../app/actions.native';
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import { isFatalJitsiConnectionError } from '../../../lib-jitsi-meet/functions.native';
|
||||
import { connect } from '../../../redux/functions';
|
||||
import { hideDialog } from '../../actions';
|
||||
// @ts-ignore
|
||||
import logger from '../../logger';
|
||||
|
||||
// @ts-ignore
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link PageReloadDialog}.
|
||||
*/
|
||||
interface IPageReloadDialogProps extends WithTranslation {
|
||||
dispatch: Dispatch<any>;
|
||||
isNetworkFailure: boolean;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of
|
||||
* {@link PageReloadDialog}.
|
||||
*/
|
||||
interface IPageReloadDialogState {
|
||||
timeLeft: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React Component that is shown before the
|
||||
* conference is reloaded.
|
||||
* Shows a warning message and counts down towards the re-load.
|
||||
*/
|
||||
class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDialogState> {
|
||||
|
||||
// @ts-ignore
|
||||
_interval: IntervalID;
|
||||
_timeoutSeconds: number;
|
||||
|
||||
/**
|
||||
* Initializes a new PageReloadOverlay instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
* @public
|
||||
*/
|
||||
constructor(props: IPageReloadDialogProps) {
|
||||
super(props);
|
||||
|
||||
this._timeoutSeconds = 10 + randomInt(0, 20);
|
||||
|
||||
this.state = {
|
||||
timeLeft: this._timeoutSeconds
|
||||
};
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onReloadNow = this._onReloadNow.bind(this);
|
||||
this._onReconnecting = this._onReconnecting.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* React Component method that executes once component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { timeLeft } = this.state;
|
||||
|
||||
logger.info(
|
||||
`The conference will be reloaded after ${timeLeft} seconds.`
|
||||
);
|
||||
|
||||
this._interval = setInterval(() =>
|
||||
this._onReconnecting(), 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the timer interval.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
if (this._interval) {
|
||||
clearInterval(this._interval);
|
||||
this._interval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clicking of the "Cancel" button. It will navigate back to the
|
||||
* welcome page.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onCancel() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
clearInterval(this._interval);
|
||||
dispatch(appNavigate(undefined));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles automatic reconnection.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onReconnecting() {
|
||||
const { dispatch } = this.props;
|
||||
const { timeLeft } = this.state;
|
||||
|
||||
if (timeLeft === 0) {
|
||||
if (this._interval) {
|
||||
dispatch(hideDialog());
|
||||
this._onReloadNow();
|
||||
this._interval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
timeLeft: timeLeft - 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clicking on the "Reload Now" button. It will navigate to the same
|
||||
* conference URL as before immediately, without waiting for the timer to
|
||||
* kick in.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onReloadNow() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
clearInterval(this._interval);
|
||||
dispatch(reloadNow());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { isNetworkFailure, t } = this.props;
|
||||
const { timeLeft } = this.state;
|
||||
|
||||
let message, title;
|
||||
|
||||
if (isNetworkFailure) {
|
||||
title = 'dialog.conferenceDisconnectTitle';
|
||||
message = 'dialog.conferenceDisconnectMsg';
|
||||
} else {
|
||||
title = 'dialog.conferenceReloadTitle';
|
||||
message = 'dialog.conferenceReloadMsg';
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelLabel = 'dialog.Cancel'
|
||||
confirmLabel = 'dialog.rejoinNow'
|
||||
descriptionKey = { `${t(message, { seconds: timeLeft })}` }
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onReloadNow }
|
||||
title = { title } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated component's props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* isNetworkFailure: boolean,
|
||||
* reason: string
|
||||
* }}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { error: conferenceError } = state['features/base/conference'];
|
||||
const { error: configError } = state['features/base/config'];
|
||||
const { error: connectionError } = state['features/base/connection'];
|
||||
const { fatalError } = state['features/overlay'];
|
||||
|
||||
const fatalConnectionError
|
||||
// @ts-ignore
|
||||
= connectionError && isFatalJitsiConnectionError(connectionError);
|
||||
const fatalConfigError = fatalError === configError;
|
||||
|
||||
const isNetworkFailure = fatalConfigError || fatalConnectionError;
|
||||
|
||||
let reason;
|
||||
|
||||
if (conferenceError) {
|
||||
reason = `error.conference.${conferenceError.name}`;
|
||||
} else if (connectionError) {
|
||||
reason = `error.conference.${connectionError.name}`;
|
||||
} else if (configError) {
|
||||
reason = `error.config.${configError.name}`;
|
||||
} else {
|
||||
logger.error('No reload reason defined!');
|
||||
}
|
||||
|
||||
return {
|
||||
isNetworkFailure,
|
||||
reason
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(PageReloadDialog));
|
||||
@@ -5,6 +5,7 @@ export { default as ConfirmDialog } from './ConfirmDialog';
|
||||
export { default as DialogContainer } from './DialogContainer';
|
||||
export { default as AlertDialog } from './AlertDialog';
|
||||
export { default as InputDialog } from './InputDialog';
|
||||
export { default as PageReloadDialog } from './PageReloadDialog';
|
||||
|
||||
// NOTE: Some dialogs reuse the style of these base classes for consistency
|
||||
// and as we're in a /native namespace, it's safe to export the styles.
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BoxModel, createStyleSheet } from '../../styles';
|
||||
|
||||
/**
|
||||
* The React {@code Component} styles of {@code Dialog}.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
/**
|
||||
* Unified container for a consistent Dialog style.
|
||||
*/
|
||||
dialogContainer: {
|
||||
paddingHorizontal: BoxModel.padding,
|
||||
paddingVertical: 1.5 * BoxModel.padding
|
||||
}
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* Placeholder styles for web to be able to use cross platform components
|
||||
* unmodified such as {@code DialogContent}.
|
||||
*/
|
||||
export default {};
|
||||
@@ -5,11 +5,6 @@ import { Component } from 'react';
|
||||
*/
|
||||
export interface IProps {
|
||||
|
||||
/**
|
||||
* Function that closes the dialog.
|
||||
*/
|
||||
closeDialog: Function;
|
||||
|
||||
/**
|
||||
* Callback to invoke on change.
|
||||
*/
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from '../../../redux';
|
||||
import AbstractDialog from '../AbstractDialog';
|
||||
import type { Props as AbstractDialogProps, State } from '../AbstractDialog';
|
||||
|
||||
import StatelessDialog from './StatelessDialog';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Dialog}.
|
||||
*/
|
||||
type Props = AbstractDialogProps & {
|
||||
|
||||
/**
|
||||
* True if listening for the Enter key should be disabled.
|
||||
*/
|
||||
disableEnter: boolean,
|
||||
|
||||
/**
|
||||
* Whether the dialog is modal. This means clicking on the blanket will
|
||||
* leave the dialog open. No cancel button.
|
||||
*/
|
||||
isModal: boolean,
|
||||
|
||||
/**
|
||||
* Disables rendering of the submit button.
|
||||
*/
|
||||
submitDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Width of the dialog, can be:
|
||||
* - 'small' (400px), 'medium' (600px), 'large' (800px),
|
||||
* 'x-large' (968px)
|
||||
* - integer value for pixel width
|
||||
* - string value for percentage.
|
||||
*/
|
||||
width: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Web dialog that uses atlaskit modal-dialog to display dialogs.
|
||||
*/
|
||||
class Dialog extends AbstractDialog<Props, State> {
|
||||
/**
|
||||
* Initializes a new Dialog instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const props = {
|
||||
...this.props,
|
||||
onCancel: this._onCancel,
|
||||
onSubmit: this._onSubmit
|
||||
};
|
||||
|
||||
// $FlowExpectedError
|
||||
delete props.dispatch;
|
||||
|
||||
return <StatelessDialog { ...props } />;
|
||||
}
|
||||
|
||||
_onCancel: () => void;
|
||||
|
||||
/**
|
||||
* Dispatches action to hide the dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
this.props.isModal || super._onCancel();
|
||||
}
|
||||
|
||||
_onSubmit: (?string) => void;
|
||||
}
|
||||
|
||||
export default connect()(Dialog);
|
||||
@@ -1,256 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import Tabs from '@atlaskit/tabs';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import logger from '../../logger';
|
||||
|
||||
import StatelessDialog from './StatelessDialog';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DialogWithTabs}.
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* Function that closes the dialog.
|
||||
*/
|
||||
closeDialog: Function,
|
||||
|
||||
/**
|
||||
* Css class name that will be added to the dialog.
|
||||
*/
|
||||
cssClassName: string,
|
||||
|
||||
/**
|
||||
* Which settings tab should be initially displayed. If not defined then
|
||||
* the first tab will be displayed.
|
||||
*/
|
||||
defaultTab: number,
|
||||
|
||||
/**
|
||||
* Disables dismissing the dialog when the blanket is clicked. Enabled
|
||||
* by default.
|
||||
*/
|
||||
disableBlanketClickDismiss: boolean,
|
||||
|
||||
/**
|
||||
* Callback invoked when the Save button has been pressed.
|
||||
*/
|
||||
onSubmit: Function,
|
||||
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Information about the tabs that will be rendered.
|
||||
*/
|
||||
tabs: Array<Object>,
|
||||
|
||||
/**
|
||||
* Key to use for showing a title.
|
||||
*/
|
||||
titleKey: string
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link DialogWithTabs}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The index of the tab that should be displayed.
|
||||
*/
|
||||
selectedTab: number,
|
||||
|
||||
/**
|
||||
* An array of the states of the tabs.
|
||||
*/
|
||||
tabStates: Array<Object>
|
||||
};
|
||||
|
||||
/**
|
||||
* A React {@code Component} for displaying a dialog with tabs.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class DialogWithTabs extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new {@code DialogWithTabs} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedTab: this.props.defaultTab || 0,
|
||||
tabStates: this.props.tabs.map(tab => tab.props)
|
||||
};
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onTabSelected = this._onTabSelected.bind(this);
|
||||
this._onTabStateChange = this._onTabStateChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const onCancel = this.props.closeDialog;
|
||||
|
||||
return (
|
||||
<StatelessDialog
|
||||
disableBlanketClickDismiss
|
||||
= { this.props.disableBlanketClickDismiss }
|
||||
onCancel = { onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = { this.props.titleKey } >
|
||||
<div className = { this.props.cssClassName } >
|
||||
{ this._renderTabs() }
|
||||
</div>
|
||||
</StatelessDialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the props to pass into the tab component.
|
||||
*
|
||||
* @param {number} tabId - The index of the tab configuration within
|
||||
* {@link this.state.tabStates}.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getTabProps(tabId) {
|
||||
const { tabs } = this.props;
|
||||
const { tabStates } = this.state;
|
||||
const tabConfiguration = tabs[tabId];
|
||||
const currentTabState = tabStates[tabId];
|
||||
|
||||
if (tabConfiguration.propsUpdateFunction) {
|
||||
return tabConfiguration.propsUpdateFunction(
|
||||
currentTabState,
|
||||
tabConfiguration.props);
|
||||
}
|
||||
|
||||
return { ...currentTabState };
|
||||
}
|
||||
|
||||
_onTabSelected: (Object, number) => void;
|
||||
|
||||
/**
|
||||
* Callback invoked when the desired tab to display should be changed.
|
||||
*
|
||||
* @param {Object} tab - The configuration passed into atlaskit tabs to
|
||||
* describe how to display the selected tab.
|
||||
* @param {number} tabIndex - The index of the tab within the array of
|
||||
* displayed tabs.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTabSelected(tab, tabIndex) { // eslint-disable-line no-unused-vars
|
||||
this.setState({ selectedTab: tabIndex });
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the tabs from the tab information passed on props.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_renderTabs() {
|
||||
const { t, tabs } = this.props;
|
||||
|
||||
if (tabs.length === 1) {
|
||||
return this._renderTab({
|
||||
...tabs[0],
|
||||
tabId: 0
|
||||
});
|
||||
}
|
||||
|
||||
if (tabs.length > 1) {
|
||||
return (
|
||||
<Tabs
|
||||
onSelect = { this._onTabSelected }
|
||||
selected = { this.state.selectedTab }
|
||||
tabs = {
|
||||
tabs.map(({ component, label, styles }, idx) => {
|
||||
return {
|
||||
content: this._renderTab({
|
||||
component,
|
||||
styles,
|
||||
tabId: idx
|
||||
}),
|
||||
label: t(label)
|
||||
};
|
||||
})
|
||||
} />);
|
||||
}
|
||||
|
||||
logger.warn('No settings tabs configured to display.');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a tab from the tab information passed as parameters.
|
||||
*
|
||||
* @param {Object} tabInfo - Information about the tab.
|
||||
* @returns {Component} - The tab.
|
||||
*/
|
||||
_renderTab({ component, styles, tabId }) {
|
||||
const { closeDialog } = this.props;
|
||||
const TabComponent = component;
|
||||
|
||||
return (
|
||||
<div className = { styles }>
|
||||
<TabComponent
|
||||
closeDialog = { closeDialog }
|
||||
mountCallback = { this.props.tabs[tabId].onMount }
|
||||
onTabStateChange
|
||||
= { this._onTabStateChange }
|
||||
tabId = { tabId }
|
||||
{ ...this._getTabProps(tabId) } />
|
||||
</div>);
|
||||
}
|
||||
|
||||
_onTabStateChange: (number, Object) => void;
|
||||
|
||||
/**
|
||||
* Changes the state for a tab.
|
||||
*
|
||||
* @param {number} tabId - The id of the tab which state will be changed.
|
||||
* @param {Object} state - The new state.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTabStateChange(tabId, state) {
|
||||
const tabStates = [ ...this.state.tabStates ];
|
||||
|
||||
tabStates[tabId] = state;
|
||||
this.setState({ tabStates });
|
||||
}
|
||||
|
||||
_onSubmit: () => void;
|
||||
|
||||
/**
|
||||
* Submits the information filled in the dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { onSubmit, tabs } = this.props;
|
||||
|
||||
tabs.forEach(({ submit }, idx) => {
|
||||
submit && submit(this.state.tabStates[idx]);
|
||||
});
|
||||
|
||||
onSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(DialogWithTabs);
|
||||
@@ -1,172 +0,0 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import ErrorIcon from '@atlaskit/icon/glyph/error';
|
||||
import WarningIcon from '@atlaskit/icon/glyph/warning';
|
||||
import {
|
||||
Header,
|
||||
Title,
|
||||
TitleText,
|
||||
titleIconWrapperStyles
|
||||
// @ts-ignore
|
||||
} from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import { IconCloseLarge } from '../../../icons/svg';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import Button from '../../../ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../../ui/constants.web';
|
||||
|
||||
const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning'; }) => {
|
||||
if (!appearance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const IconSymbol = appearance === 'danger' ? ErrorIcon : WarningIcon;
|
||||
|
||||
return (
|
||||
<span css = { titleIconWrapperStyles(appearance) }>
|
||||
<IconSymbol label = { `${appearance} icon` } />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
appearance?: 'danger' | 'warning';
|
||||
classes: any;
|
||||
heading: string;
|
||||
hideCloseIconButton: boolean;
|
||||
id?: string;
|
||||
isHeadingMultiline: boolean;
|
||||
onClose: (e?: any) => void;
|
||||
showKeyline: boolean;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @param {Object} theme - The current UI theme.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
closeButton: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
cursor: 'pointer',
|
||||
padding: 13,
|
||||
|
||||
[theme.breakpoints.down(480)]: {
|
||||
background: theme.palette.action02
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
background: theme.palette.ui04
|
||||
}
|
||||
},
|
||||
header: {
|
||||
boxShadow: 'none',
|
||||
|
||||
'& h4': {
|
||||
...withPixelLineHeight(theme.typography.heading5),
|
||||
color: theme.palette.text01
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A default header for modal-dialog components.
|
||||
*
|
||||
* @class ModalHeader
|
||||
* @augments {React.Component<IProps>}
|
||||
*/
|
||||
class ModalHeader extends React.Component<IProps> {
|
||||
static defaultProps = {
|
||||
isHeadingMultiline: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ModalHeader} instance.
|
||||
*
|
||||
* @param {*} props - The read-only properties with which the new instance
|
||||
* is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onKeyPress = this._onKeyPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPress(e: React.KeyboardEvent) {
|
||||
if (this.props.onClose && (e.key === ' ' || e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
appearance,
|
||||
classes,
|
||||
heading,
|
||||
hideCloseIconButton,
|
||||
onClose,
|
||||
showKeyline,
|
||||
isHeadingMultiline,
|
||||
testId,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
if (!heading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Header
|
||||
className = { classes.header }
|
||||
showKeyline = { showKeyline }>
|
||||
<Title>
|
||||
<TitleIcon appearance = { appearance } />
|
||||
<TitleText
|
||||
data-testid = { testId && `${testId}-heading` }
|
||||
id = { id }
|
||||
isHeadingMultiline = { isHeadingMultiline }>
|
||||
{heading}
|
||||
</TitleText>
|
||||
</Title>
|
||||
|
||||
{
|
||||
!hideCloseIconButton && <Button
|
||||
accessibilityLabel = { t('dialog.close') }
|
||||
icon = { IconCloseLarge }
|
||||
id = 'modal-header-close-button'
|
||||
onClick = { onClose }
|
||||
size = 'large'
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
}
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default translate(withStyles(styles)(ModalHeader));
|
||||
@@ -1,377 +0,0 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
import Modal, { ModalFooter } from '@atlaskit/modal-dialog';
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React, { Component, ReactElement } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import Button from '../../../ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../../ui/constants.web';
|
||||
import type { DialogProps } from '../../constants';
|
||||
|
||||
import ModalHeader from './ModalHeader';
|
||||
|
||||
/**
|
||||
* The ID to be used for the cancel button if enabled.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const CANCEL_BUTTON_ID = 'modal-dialog-cancel-button';
|
||||
|
||||
/**
|
||||
* The ID to be used for the ok button if enabled.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const OK_BUTTON_ID = 'modal-dialog-ok-button';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link StatelessDialog}.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
interface IProps extends DialogProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Custom dialog header that replaces the standard heading.
|
||||
*/
|
||||
customHeader?: ReactElement<any> | Function;
|
||||
|
||||
/**
|
||||
* Disables dismissing the dialog when the blanket is clicked. Enabled
|
||||
* by default.
|
||||
*/
|
||||
disableBlanketClickDismiss: boolean;
|
||||
|
||||
/*
|
||||
* True if listening for the Enter key should be disabled.
|
||||
*/
|
||||
disableEnter: boolean;
|
||||
|
||||
/**
|
||||
* If true, no footer will be displayed.
|
||||
*/
|
||||
disableFooter?: boolean;
|
||||
|
||||
/**
|
||||
* If true, the cancel button will not display but cancel actions, like
|
||||
* clicking the blanket, will cancel.
|
||||
*/
|
||||
hideCancelButton: boolean;
|
||||
|
||||
/**
|
||||
* If true, the close icon button will not be displayed.
|
||||
*/
|
||||
hideCloseIconButton: boolean;
|
||||
|
||||
/**
|
||||
* Whether the dialog is modal. This means clicking on the blanket will
|
||||
* leave the dialog open. No cancel button.
|
||||
*/
|
||||
isModal: boolean;
|
||||
|
||||
/**
|
||||
* The handler for the event when clicking the 'confirmNo' button.
|
||||
* Defaults to onCancel if absent.
|
||||
*/
|
||||
onDecline?: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked when setting the ref of the Dialog.
|
||||
*/
|
||||
onDialogRef?: Function;
|
||||
|
||||
/**
|
||||
* Disables rendering of the submit button.
|
||||
*/
|
||||
submitDisabled: boolean;
|
||||
|
||||
/**
|
||||
* Width of the dialog, can be:
|
||||
* - 'small' (400px), 'medium' (600px), 'large' (800px),
|
||||
* 'x-large' (968px)
|
||||
* - integer value for pixel width
|
||||
* - string value for percentage.
|
||||
*/
|
||||
width: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @param {Object} theme - The theme.
|
||||
* @returns {Object}
|
||||
*/
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
footer: {
|
||||
boxShadow: 'none'
|
||||
},
|
||||
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
|
||||
'& > button:first-child': {
|
||||
marginRight: theme.spacing(2)
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Web dialog that uses atlaskit modal-dialog to display dialogs.
|
||||
*/
|
||||
class StatelessDialog extends Component<IProps> {
|
||||
static defaultProps = {
|
||||
hideCloseIconButton: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code StatelessDialog} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onDialogDismissed = this._onDialogDismissed.bind(this);
|
||||
this._onKeyPress = this._onKeyPress.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._renderFooter = this._renderFooter.bind(this);
|
||||
this._onDialogRef = this._onDialogRef.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
customHeader,
|
||||
children,
|
||||
hideCloseIconButton,
|
||||
t,
|
||||
titleString,
|
||||
titleKey,
|
||||
width
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
autoFocus = { true }
|
||||
components = {{
|
||||
// @ts-ignore
|
||||
Header: customHeader ? customHeader : props => (
|
||||
// @ts-ignore
|
||||
<ModalHeader
|
||||
{ ...props }
|
||||
heading = { titleString || t(titleKey ?? '') }
|
||||
hideCloseIconButton = { hideCloseIconButton } />
|
||||
)
|
||||
}}
|
||||
footer = { this._renderFooter }
|
||||
i18n = { this.props.i18n }
|
||||
onClose = { this._onDialogDismissed }
|
||||
onDialogDismissed = { this._onDialogDismissed }
|
||||
shouldCloseOnEscapePress = { true }
|
||||
width = { width || 'medium' }>
|
||||
<div
|
||||
onKeyPress = { this._onKeyPress }
|
||||
ref = { this._onDialogRef }>
|
||||
<form
|
||||
className = 'modal-dialog-form'
|
||||
id = 'modal-dialog-form'
|
||||
onSubmit = { this._onSubmit }>
|
||||
{ children }
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a ReactElement to display buttons for closing the modal.
|
||||
*
|
||||
* @param {Object} propsFromModalFooter - The props passed in from the
|
||||
* {@link ModalFooter} component.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFooter(propsFromModalFooter: any) {
|
||||
// Filter out falsy (null) values because {@code ButtonGroup} will error
|
||||
// if passed in anything but buttons with valid type props.
|
||||
const buttons = [
|
||||
this._renderCancelButton(),
|
||||
this._renderOKButton()
|
||||
].filter(Boolean);
|
||||
|
||||
if (this.props.disableFooter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalFooter
|
||||
className = { this.props.classes.footer }
|
||||
showKeyline = { propsFromModalFooter.showKeyline } >
|
||||
{
|
||||
|
||||
/**
|
||||
* Atlaskit has this empty span (JustifySim) so...
|
||||
*/
|
||||
}
|
||||
<span />
|
||||
<div className = { this.props.classes.buttonContainer }>
|
||||
{ buttons }
|
||||
</div>
|
||||
</ModalFooter>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches action to hide the dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
if (!this.props.isModal) {
|
||||
const { onCancel } = this.props;
|
||||
|
||||
onCancel?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click on the blanket area.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDialogDismissed() {
|
||||
if (!this.props.disableBlanketClickDismiss) {
|
||||
this._onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the action when submitting the dialog.
|
||||
*
|
||||
* @private
|
||||
* @param {string} value - The submitted value if any.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit(value?: any) {
|
||||
const { onSubmit } = this.props;
|
||||
|
||||
onSubmit?.(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders Cancel button.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement|null} The Cancel button if enabled and dialog is
|
||||
* not modal.
|
||||
*/
|
||||
_renderCancelButton() {
|
||||
if (this.props.cancelDisabled
|
||||
|| this.props.isModal
|
||||
|| this.props.hideCancelButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
t,
|
||||
onDecline
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
accessibilityLabel = { t(this.props.cancelKey || 'dialog.Cancel') }
|
||||
id = { CANCEL_BUTTON_ID }
|
||||
key = { CANCEL_BUTTON_ID }
|
||||
label = { t(this.props.cancelKey || 'dialog.Cancel') }
|
||||
onClick = { onDecline || this._onCancel }
|
||||
size = 'small'
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders OK button.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement|null} The OK button if enabled.
|
||||
*/
|
||||
_renderOKButton() {
|
||||
const {
|
||||
submitDisabled,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
if (submitDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
accessibilityLabel = { t(this.props.okKey || 'dialog.Ok') }
|
||||
disabled = { this.props.okDisabled }
|
||||
id = { OK_BUTTON_ID }
|
||||
key = { OK_BUTTON_ID }
|
||||
label = { t(this.props.okKey || 'dialog.Ok') }
|
||||
onClick = { this._onSubmit }
|
||||
size = 'small' />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when setting the ref of the dialog's child passing the Modal ref.
|
||||
* It is done this way because we cannot directly access the ref of the Modal component.
|
||||
*
|
||||
* @param {HTMLElement} element - The DOM element for the dialog.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDialogRef(element?: any) {
|
||||
this.props.onDialogRef?.(element?.parentNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles 'Enter' key in the dialog to submit/hide dialog depending on
|
||||
* the available buttons and their disabled state.
|
||||
*
|
||||
* @param {Object} event - The key event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPress(event: React.KeyboardEvent) {
|
||||
// If the event coming to the dialog has been subject to preventDefault
|
||||
// we don't handle it here.
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && !this.props.disableEnter) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.props.submitDisabled && !this.props.cancelDisabled) {
|
||||
this._onCancel();
|
||||
} else if (!this.props.okDisabled) {
|
||||
this._onSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(withStyles(styles)(StatelessDialog));
|
||||
@@ -1,31 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
FillScreen,
|
||||
PositionerAbsolute,
|
||||
PositionerRelative,
|
||||
dialogHeight,
|
||||
dialogWidth
|
||||
} from '@atlaskit/modal-dialog/dist/es2019/styled/Modal.js';
|
||||
import { DN50, N0 } from '@atlaskit/theme/colors';
|
||||
import { themed } from '@atlaskit/theme/components';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
isChromeless: boolean
|
||||
}
|
||||
|
||||
const ThemedDialog = (props: Props) => {
|
||||
const style = { backgroundColor: props.isChromeless ? 'transparent' : themed({ light: N0,
|
||||
dark: DN50 })({ theme: { mode: 'dark' } }) };
|
||||
|
||||
return (<Dialog
|
||||
{ ...props }
|
||||
aria-modal = { true }
|
||||
style = { style }
|
||||
theme = {{ mode: 'dark' }} />);
|
||||
};
|
||||
|
||||
|
||||
export { ThemedDialog as Dialog, FillScreen, dialogWidth, dialogHeight, PositionerAbsolute, PositionerRelative };
|
||||
@@ -2,7 +2,4 @@
|
||||
|
||||
export { default as AbstractDialogTab } from './AbstractDialogTab';
|
||||
export type { Props as AbstractDialogTabProps } from './AbstractDialogTab';
|
||||
export { default as Dialog } from './Dialog-old';
|
||||
export { default as DialogWithTabs } from './DialogWithTabs';
|
||||
export { default as StatelessDialog } from './StatelessDialog';
|
||||
export { default as DialogContainer } from '../../../ui/components/web/DialogContainer';
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
import LoginDialog from '../../authentication/components/web/LoginDialog';
|
||||
import WaitForOwnerDialog from '../../authentication/components/web/WaitForOwnerDialog';
|
||||
import ChatPrivacyDialog from '../../chat/components/web/ChatPrivacyDialog';
|
||||
import DesktopPicker from '../../desktop-picker/components/DesktopPicker';
|
||||
import DisplayNamePrompt from '../../display-name/components/web/DisplayNamePrompt';
|
||||
import ParticipantVerificationDialog from '../../e2ee/components/ParticipantVerificationDialog';
|
||||
import EmbedMeetingDialog from '../../embed-meeting/components/EmbedMeetingDialog';
|
||||
// @ts-ignore
|
||||
import FeedbackDialog from '../../feedback/components/FeedbackDialog.web';
|
||||
import AddPeopleDialog from '../../invite/components/add-people-dialog/web/AddPeopleDialog';
|
||||
import PremiumFeatureDialog from '../../jaas/components/web/PremiumFeatureDialog';
|
||||
import KeyboardShortcutsDialog from '../../keyboard-shortcuts/components/web/KeyboardShortcutsDialog';
|
||||
// @ts-ignore
|
||||
import StartLiveStreamDialog from '../../recording/components/LiveStream/web/StartLiveStreamDialog';
|
||||
// @ts-ignore
|
||||
import StopLiveStreamDialog from '../../recording/components/LiveStream/web/StopLiveStreamDialog';
|
||||
// @ts-ignore
|
||||
import StartRecordingDialog from '../../recording/components/Recording/web/StartRecordingDialog';
|
||||
// @ts-ignore
|
||||
import StopRecordingDialog from '../../recording/components/Recording/web/StopRecordingDialog';
|
||||
// @ts-ignore
|
||||
import RemoteControlAuthorizationDialog from '../../remote-control/components/RemoteControlAuthorizationDialog';
|
||||
import PasswordRequiredPrompt from '../../room-lock/components/PasswordRequiredPrompt.web';
|
||||
import SalesforceLinkDialog from '../../salesforce/components/web/SalesforceLinkDialog';
|
||||
import ShareAudioDialog from '../../screen-share/components/web/ShareAudioDialog';
|
||||
import ShareScreenWarningDialog from '../../screen-share/components/web/ShareScreenWarningDialog';
|
||||
import SecurityDialog from '../../security/components/security-dialog/web/SecurityDialog';
|
||||
import LogoutDialog from '../../settings/components/web/LogoutDialog';
|
||||
import SharedVideoDialog from '../../shared-video/components/web/SharedVideoDialog';
|
||||
import SpeakerStats from '../../speaker-stats/components/web/SpeakerStats';
|
||||
import LanguageSelectorDialog from '../../subtitles/components/LanguageSelectorDialog.web';
|
||||
import GrantModeratorDialog from '../../video-menu/components/web/GrantModeratorDialog';
|
||||
import KickRemoteParticipantDialog from '../../video-menu/components/web/KickRemoteParticipantDialog';
|
||||
import MuteEveryoneDialog from '../../video-menu/components/web/MuteEveryoneDialog';
|
||||
import MuteEveryonesVideoDialog from '../../video-menu/components/web/MuteEveryonesVideoDialog';
|
||||
import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
|
||||
// @ts-ignore
|
||||
import VideoQualityDialog from '../../video-quality/components/VideoQualityDialog.web';
|
||||
import VirtualBackgroundDialog from '../../virtual-background/components/VirtualBackgroundDialog';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
|
||||
import { OPEN_DIALOG } from './actionTypes';
|
||||
|
||||
// ! IMPORTANT - This whole middleware is only needed for the transition from from @atlaskit dialog to our component.
|
||||
// ! It should be removed when the transition is over.
|
||||
|
||||
const NEW_DIALOG_LIST = [ KeyboardShortcutsDialog, ChatPrivacyDialog, DisplayNamePrompt, EmbedMeetingDialog,
|
||||
FeedbackDialog, AddPeopleDialog, PremiumFeatureDialog, StartLiveStreamDialog, StopLiveStreamDialog,
|
||||
StartRecordingDialog, StopRecordingDialog, ShareAudioDialog, ShareScreenWarningDialog, SecurityDialog,
|
||||
SharedVideoDialog, SpeakerStats, LanguageSelectorDialog, MuteEveryoneDialog, MuteEveryonesVideoDialog,
|
||||
GrantModeratorDialog, KickRemoteParticipantDialog, MuteRemoteParticipantsVideoDialog, VideoQualityDialog,
|
||||
VirtualBackgroundDialog, LoginDialog, WaitForOwnerDialog, DesktopPicker, RemoteControlAuthorizationDialog,
|
||||
LogoutDialog, SalesforceLinkDialog, ParticipantVerificationDialog, PasswordRequiredPrompt ];
|
||||
|
||||
// This function is necessary while the transition from @atlaskit dialog to our component is ongoing.
|
||||
const isNewDialog = (component: any) => NEW_DIALOG_LIST.some(comp => comp === component);
|
||||
|
||||
/**
|
||||
* Implements the entry point of the middleware of the feature base/media.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(() => (next: Function) => (action: any) => {
|
||||
switch (action.type) {
|
||||
case OPEN_DIALOG: {
|
||||
action.isNewDialog = isNewDialog(action.component);
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
export interface IDialogState {
|
||||
component?: ComponentType;
|
||||
componentProps?: Object;
|
||||
isNewDialog?: boolean;
|
||||
sheet?: ComponentType;
|
||||
sheetProps?: Object;
|
||||
}
|
||||
@@ -44,8 +43,7 @@ ReducerRegistry.register<IDialogState>('features/base/dialog', (state = {}, acti
|
||||
case OPEN_DIALOG:
|
||||
return assign(state, {
|
||||
component: action.component,
|
||||
componentProps: action.componentProps,
|
||||
isNewDialog: action.isNewDialog
|
||||
componentProps: action.componentProps
|
||||
});
|
||||
|
||||
case HIDE_SHEET:
|
||||
|
||||
3
react/features/base/icons/svg/bell.svg
Normal file
3
react/features/base/icons/svg/bell.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 1.5C11.5858 1.5 11.25 1.83579 11.25 2.25V3C11.25 3.01239 11.2503 3.0247 11.2509 3.03694C7.46047 3.41282 4.5 6.61068 4.5 10.5V12.0949C4.5 12.9852 4.10453 13.8296 3.42055 14.3995L3.07702 14.6858C2.55299 15.1225 2.25 15.7694 2.25 16.4515C2.25 17.7209 3.27906 18.75 4.54846 18.75H8.25C8.25 20.8211 9.92893 22.5 12 22.5C14.0711 22.5 15.75 20.8211 15.75 18.75H19.3635C20.6815 18.75 21.75 17.6815 21.75 16.3635C21.75 15.7306 21.4986 15.1236 21.051 14.676L20.3787 14.0037C19.8161 13.4411 19.5 12.678 19.5 11.8824V10.5C19.5 6.61068 16.5395 3.41282 12.7491 3.03694C12.7497 3.0247 12.75 3.01239 12.75 3V2.25C12.75 1.83579 12.4142 1.5 12 1.5ZM18 10.5C18 7.18629 15.3137 4.5 12 4.5C8.68629 4.5 6 7.18629 6 10.5V12.7974C6 13.6878 5.60453 14.5321 4.92055 15.1021L4.0373 15.8381C3.85526 15.9898 3.75 16.2146 3.75 16.4515C3.75 16.8925 4.10748 17.25 4.54846 17.25H19.3635C19.8531 17.25 20.25 16.8531 20.25 16.3635C20.25 16.1284 20.1566 15.9029 19.9904 15.7367L18.8787 14.625C18.3161 14.0624 18 13.2993 18 12.5037V10.5ZM14.25 18.75H9.75C9.75 19.9926 10.7574 21 12 21C13.2426 21 14.25 19.9926 14.25 18.75Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -8,6 +8,7 @@ export { default as IconArrowUpLarge } from './arrow-up-large.svg';
|
||||
export { default as IconAudioOnly } from './visibility.svg';
|
||||
export { default as IconAudioOnlyOff } from './visibility-off.svg';
|
||||
export { default as IconBluetooth } from './bluetooth.svg';
|
||||
export { default as IconBell } from './bell.svg';
|
||||
export { default as IconCalendar } from './calendar.svg';
|
||||
export { default as IconCameraRefresh } from './camera-refresh.svg';
|
||||
export { default as IconCar } from './car.svg';
|
||||
@@ -88,6 +89,7 @@ export { default as IconTileView } from './tile-view.svg';
|
||||
export { default as IconTrash } from './trash.svg';
|
||||
export { default as IconUserDeleted } from './user-deleted.svg';
|
||||
export { default as IconUsers } from './users.svg';
|
||||
export { default as IconUser } from './user.svg';
|
||||
export { default as IconVideo } from './video.svg';
|
||||
export { default as IconVideoOff } from './video-off.svg';
|
||||
export { default as IconVolumeOff } from './volume-off.svg';
|
||||
|
||||
4
react/features/base/icons/svg/user.svg
Normal file
4
react/features/base/icons/svg/user.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5 6.75C16.5 9.23528 14.4853 11.25 12 11.25C9.51472 11.25 7.5 9.23528 7.5 6.75C7.5 4.26472 9.51472 2.25 12 2.25C14.4853 2.25 16.5 4.26472 16.5 6.75ZM15 6.75C15 8.40685 13.6569 9.75 12 9.75C10.3431 9.75 9 8.40685 9 6.75C9 5.09315 10.3431 3.75 12 3.75C13.6569 3.75 15 5.09315 15 6.75Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5 17.25C19.5 19.7353 16.1421 21.75 12 21.75C7.85786 21.75 4.5 19.7353 4.5 17.25C4.5 14.7647 7.85786 12.75 12 12.75C16.1421 12.75 19.5 14.7647 19.5 17.25ZM18 17.25C18 17.7588 17.6485 18.4756 16.5316 19.1457C15.4445 19.798 13.8459 20.25 12 20.25C10.1541 20.25 8.55549 19.798 7.46844 19.1457C6.35154 18.4756 6 17.7588 6 17.25C6 16.7412 6.35154 16.0244 7.46844 15.3543C8.55549 14.702 10.1541 14.25 12 14.25C13.8459 14.25 15.4445 14.702 16.5316 15.3543C17.6485 16.0244 18 16.7412 18 17.25Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1016 B |
@@ -1,9 +1,12 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import { IStateful } from '../app/types';
|
||||
import { toState } from '../redux/functions';
|
||||
|
||||
// @ts-ignore
|
||||
import JitsiMeetJS from './_';
|
||||
|
||||
|
||||
const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
|
||||
const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
|
||||
// @ts-ignore
|
||||
import Video from './web/Video';
|
||||
|
||||
export default Video;
|
||||
@@ -1,210 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { getFieldValue } from '../../../react';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* If the input should be focused on display.
|
||||
*/
|
||||
autoFocus?: boolean,
|
||||
|
||||
/**
|
||||
* Class name to be appended to the default class list.
|
||||
*/
|
||||
className?: string,
|
||||
|
||||
/**
|
||||
* TestId of the button. Can be used to locate element when testing UI.
|
||||
*/
|
||||
testId?: string,
|
||||
|
||||
/**
|
||||
* Callback for the onChange event of the field.
|
||||
*/
|
||||
onChange: Function,
|
||||
|
||||
/**
|
||||
* Callback to be used when the user hits Enter in the field.
|
||||
*/
|
||||
onSubmit?: Function,
|
||||
|
||||
/**
|
||||
* Placeholder text for the field.
|
||||
*/
|
||||
placeHolder: string,
|
||||
|
||||
/**
|
||||
* Whether the input is read only or not.
|
||||
*/
|
||||
readOnly?: boolean,
|
||||
|
||||
/**
|
||||
* The field type (e.g. Text, password...etc).
|
||||
*/
|
||||
type: string,
|
||||
|
||||
/**
|
||||
* Externally provided value.
|
||||
*/
|
||||
value?: string,
|
||||
id?: string,
|
||||
autoComplete?: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* True if the field is focused, false otherwise.
|
||||
*/
|
||||
focused: boolean,
|
||||
|
||||
/**
|
||||
* The current value of the field.
|
||||
*/
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a pre-styled input field to be used on pre-meeting screens.
|
||||
*/
|
||||
export default class InputField extends PureComponent<Props, State> {
|
||||
static defaultProps: {
|
||||
className: '',
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
focused: false,
|
||||
value: props.value || ''
|
||||
};
|
||||
|
||||
this._onBlur = this._onBlur.bind(this);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this._onFocus = this._onFocus.bind(this);
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent.getDerivedStateFromProps}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
const { value } = props;
|
||||
|
||||
if (state.value !== value) {
|
||||
return {
|
||||
...state,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<input
|
||||
autoComplete = { this.props.autoComplete }
|
||||
autoFocus = { this.props.autoFocus }
|
||||
className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` }
|
||||
data-testid = { this.props.testId ? this.props.testId : undefined }
|
||||
id = { this.props.id }
|
||||
onBlur = { this._onBlur }
|
||||
onChange = { this._onChange }
|
||||
onFocus = { this._onFocus }
|
||||
onKeyDown = { this._onKeyDown }
|
||||
onKeyPress = { this._onKeyPress }
|
||||
placeholder = { this.props.placeHolder }
|
||||
readOnly = { this.props.readOnly }
|
||||
type = { this.props.type }
|
||||
value = { this.state.value } />
|
||||
);
|
||||
}
|
||||
|
||||
_onBlur: () => void;
|
||||
|
||||
/**
|
||||
* Callback for the onBlur event of the field.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onBlur() {
|
||||
this.setState({
|
||||
focused: false
|
||||
});
|
||||
}
|
||||
|
||||
_onChange: Object => void;
|
||||
|
||||
/**
|
||||
* Callback for the onChange event of the field.
|
||||
*
|
||||
* @param {Object} evt - The static event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChange(evt) {
|
||||
const value = getFieldValue(evt);
|
||||
|
||||
this.setState({
|
||||
value
|
||||
});
|
||||
|
||||
const { onChange } = this.props;
|
||||
|
||||
onChange && onChange(value);
|
||||
}
|
||||
|
||||
_onFocus: () => void;
|
||||
|
||||
/**
|
||||
* Callback for the onFocus event of the field.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFocus() {
|
||||
this.setState({
|
||||
focused: true
|
||||
});
|
||||
}
|
||||
|
||||
_onKeyDown: Object => void;
|
||||
|
||||
/**
|
||||
* Joins the conference on 'Enter'.
|
||||
*
|
||||
* @param {Event} event - Key down event object.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyDown(event) {
|
||||
const { onSubmit } = this.props;
|
||||
|
||||
onSubmit && event.key === 'Enter' && onSubmit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop event propagation on key press.
|
||||
*
|
||||
* @param {Event} event - Key press event object.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPress(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
|
||||
export { default as ActionButton } from './ActionButton';
|
||||
export { default as InputField } from './InputField';
|
||||
export { default as PreMeetingScreen } from './PreMeetingScreen';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { GlobalStyles as MUIGlobalStyles } from 'tss-react';
|
||||
import { useStyles } from 'tss-react/mui';
|
||||
|
||||
import { commonStyles, getGlobalStyles } from '../constants';
|
||||
import { commonStyles } from '../constants';
|
||||
|
||||
/**
|
||||
* A component generating all the global styles.
|
||||
@@ -14,8 +14,7 @@ function GlobalStyles() {
|
||||
|
||||
return (<MUIGlobalStyles
|
||||
styles = {{
|
||||
...commonStyles(theme),
|
||||
...getGlobalStyles(theme)
|
||||
...commonStyles(theme)
|
||||
}} />);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ import { IButtonProps } from '../types';
|
||||
import styles from './buttonStyles';
|
||||
|
||||
export interface IProps extends IButtonProps {
|
||||
color?: string;
|
||||
color?: string | undefined;
|
||||
contentStyle?: Object | undefined;
|
||||
labelStyle?: Object | undefined;
|
||||
mode?: any;
|
||||
style?: Object | undefined;
|
||||
useRippleColor?: boolean;
|
||||
}
|
||||
|
||||
const Button: React.FC<IProps> = ({
|
||||
@@ -27,31 +29,36 @@ const Button: React.FC<IProps> = ({
|
||||
icon,
|
||||
labelKey,
|
||||
labelStyle,
|
||||
mode = BUTTON_MODES.CONTAINED,
|
||||
onClick: onPress,
|
||||
style,
|
||||
type
|
||||
type,
|
||||
useRippleColor = true
|
||||
}: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { CONTAINED } = BUTTON_MODES;
|
||||
const { DESTRUCTIVE, PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;
|
||||
const { CONTAINED, TEXT } = BUTTON_MODES;
|
||||
|
||||
const rippleColor
|
||||
= useRippleColor ? BaseTheme.palette.action03Active : 'transparent';
|
||||
|
||||
let buttonLabelStyles;
|
||||
let buttonStyles;
|
||||
let color;
|
||||
let mode;
|
||||
|
||||
if (type === PRIMARY) {
|
||||
buttonLabelStyles = styles.buttonLabelPrimary;
|
||||
color = BaseTheme.palette.action01;
|
||||
mode = CONTAINED;
|
||||
buttonLabelStyles = mode === TEXT
|
||||
? styles.buttonLabelPrimaryText
|
||||
: styles.buttonLabelPrimary;
|
||||
color = mode === CONTAINED && BaseTheme.palette.action01;
|
||||
} else if (type === SECONDARY) {
|
||||
buttonLabelStyles = styles.buttonLabelSecondary;
|
||||
color = BaseTheme.palette.action02;
|
||||
mode = CONTAINED;
|
||||
color = mode === CONTAINED && BaseTheme.palette.action02;
|
||||
} else if (type === DESTRUCTIVE) {
|
||||
color = BaseTheme.palette.actionDanger;
|
||||
buttonLabelStyles = styles.buttonLabelDestructive;
|
||||
mode = CONTAINED;
|
||||
buttonLabelStyles = mode === TEXT
|
||||
? styles.buttonLabelDestructiveText
|
||||
: styles.buttonLabelDestructive;
|
||||
color = mode === CONTAINED && BaseTheme.palette.actionDanger;
|
||||
} else {
|
||||
color = buttonColor;
|
||||
buttonLabelStyles = styles.buttonLabel;
|
||||
@@ -65,15 +72,17 @@ const Button: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
if (type === TERTIARY) {
|
||||
buttonLabelStyles
|
||||
= disabled ? styles.buttonLabelTertiaryDisabled : styles.buttonLabelTertiary;
|
||||
if (useRippleColor && disabled) {
|
||||
buttonLabelStyles = styles.buttonLabelTertiaryDisabled;
|
||||
}
|
||||
buttonLabelStyles = styles.buttonLabelTertiary;
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
accessibilityLabel = { accessibilityLabel }
|
||||
disabled = { disabled }
|
||||
onPress = { onPress }
|
||||
rippleColor = { BaseTheme.palette.action03Active }
|
||||
rippleColor = { rippleColor }
|
||||
style = { [
|
||||
buttonStyles,
|
||||
style
|
||||
|
||||
@@ -36,7 +36,7 @@ const IconButton: React.FC<IIconButtonProps> = ({
|
||||
iconButtonContainerStyles = styles.iconButtonContainerSecondary;
|
||||
rippleColor = BaseTheme.palette.action02;
|
||||
} else if (type === TERTIARY) {
|
||||
color = BaseTheme.palette.icon01;
|
||||
color = iconColor;
|
||||
iconButtonContainerStyles = styles.iconButtonContainer;
|
||||
rippleColor = BaseTheme.palette.action03;
|
||||
} else {
|
||||
|
||||
@@ -42,6 +42,11 @@ export default {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
buttonLabelPrimaryText: {
|
||||
...buttonLabel,
|
||||
color: BaseTheme.palette.action01
|
||||
},
|
||||
|
||||
buttonLabelSecondary: {
|
||||
...buttonLabel,
|
||||
color: BaseTheme.palette.text04
|
||||
@@ -52,6 +57,11 @@ export default {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
buttonLabelDestructiveText: {
|
||||
...buttonLabel,
|
||||
color: BaseTheme.palette.actionDanger
|
||||
},
|
||||
|
||||
buttonLabelTertiary: {
|
||||
...buttonLabel,
|
||||
color: BaseTheme.palette.text01,
|
||||
|
||||
199
react/features/base/ui/components/web/BaseDialog.tsx
Normal file
199
react/features/base/ui/components/web/BaseDialog.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import React, { ReactNode, useCallback, useContext, useEffect } from 'react';
|
||||
import FocusLock from 'react-focus-lock';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { keyframes } from 'tss-react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
|
||||
import { DialogTransitionContext } from './DialogTransition';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'fixed',
|
||||
color: theme.palette.text01,
|
||||
...withPixelLineHeight(theme.typography.bodyLongRegular),
|
||||
top: 0,
|
||||
left: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
zIndex: 301,
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
`} 0.2s forwards ease-out`,
|
||||
|
||||
'&.unmount': {
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`} 0.15s forwards ease-in`
|
||||
}
|
||||
},
|
||||
|
||||
backdrop: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: theme.palette.ui02,
|
||||
opacity: 0.75
|
||||
},
|
||||
|
||||
modal: {
|
||||
backgroundColor: theme.palette.ui01,
|
||||
border: `1px solid ${theme.palette.ui03}`,
|
||||
boxShadow: '0px 4px 25px 4px rgba(20, 20, 20, 0.6)',
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 'auto',
|
||||
minHeight: '200px',
|
||||
maxHeight: '80vh',
|
||||
marginTop: '64px',
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
margin-top: 85px
|
||||
}
|
||||
100% {
|
||||
margin-top: 64px
|
||||
}
|
||||
`} 0.2s forwards ease-out`,
|
||||
|
||||
'&.medium': {
|
||||
width: '400px'
|
||||
},
|
||||
|
||||
'&.large': {
|
||||
width: '664px'
|
||||
},
|
||||
|
||||
'&.unmount': {
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
margin-top: 64px
|
||||
}
|
||||
100% {
|
||||
margin-top: 40px
|
||||
}
|
||||
`} 0.15s forwards ease-in`
|
||||
},
|
||||
|
||||
'@media (max-width: 448px)': {
|
||||
width: '100% !important',
|
||||
maxHeight: 'initial',
|
||||
height: '100%',
|
||||
margin: 0,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
margin-top: 15px
|
||||
}
|
||||
100% {
|
||||
margin-top: 0
|
||||
}
|
||||
`} 0.2s forwards ease-out`,
|
||||
|
||||
'&.unmount': {
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
margin-top: 0
|
||||
}
|
||||
100% {
|
||||
margin-top: 15px
|
||||
}
|
||||
`} 0.15s forwards ease-in`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
focusLock: {
|
||||
zIndex: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export interface IProps {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
description?: string;
|
||||
disableBackdropClose?: boolean;
|
||||
disableEnter?: boolean;
|
||||
onClose?: () => void;
|
||||
size?: 'large' | 'medium';
|
||||
submit?: () => void;
|
||||
title?: string;
|
||||
titleKey?: string;
|
||||
}
|
||||
|
||||
const BaseDialog = ({
|
||||
children,
|
||||
className,
|
||||
description,
|
||||
disableBackdropClose,
|
||||
disableEnter,
|
||||
onClose,
|
||||
size = 'medium',
|
||||
submit,
|
||||
title,
|
||||
titleKey
|
||||
}: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const { isUnmounting } = useContext(DialogTransitionContext);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onBackdropClick = useCallback(() => {
|
||||
!disableBackdropClose && onClose?.();
|
||||
}, [ disableBackdropClose, onClose ]);
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose?.();
|
||||
}
|
||||
if (e.key === 'Enter' && !disableEnter) {
|
||||
submit?.();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className = { cx(classes.container, isUnmounting && 'unmount') }>
|
||||
<div
|
||||
className = { classes.backdrop }
|
||||
onClick = { onBackdropClick } />
|
||||
<FocusLock className = { classes.focusLock }>
|
||||
<div
|
||||
aria-describedby = { description }
|
||||
aria-labelledby = { title ?? t(titleKey ?? '') }
|
||||
aria-modal = { true }
|
||||
className = { cx(classes.modal, isUnmounting && 'unmount', size, className) }
|
||||
role = 'dialog'>
|
||||
{children}
|
||||
</div>
|
||||
</FocusLock>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseDialog;
|
||||
@@ -7,6 +7,7 @@ import JitsiPortal from '../../../../toolbox/components/web/JitsiPortal';
|
||||
import { showOverflowDrawer } from '../../../../toolbox/functions.web';
|
||||
import participantsPaneTheme from '../../../components/themes/participantsPaneTheme.json';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import { spacing } from '../../Tokens';
|
||||
|
||||
/**
|
||||
* Get a style property from a style declaration as a float.
|
||||
@@ -34,6 +35,9 @@ const getComputedOuterHeight = (element: HTMLElement) => {
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* ARIA attributes.
|
||||
*/
|
||||
[key: `aria-${string}`]: string;
|
||||
|
||||
/**
|
||||
@@ -106,6 +110,11 @@ interface IProps {
|
||||
*/
|
||||
onMouseLeave?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Container role.
|
||||
*/
|
||||
role?: string;
|
||||
|
||||
/**
|
||||
* Tab index for the menu.
|
||||
*/
|
||||
@@ -167,7 +176,9 @@ const ContextMenu = ({
|
||||
onDrawerClose,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
tabIndex
|
||||
role,
|
||||
tabIndex,
|
||||
...aria
|
||||
}: IProps) => {
|
||||
const [ isHidden, setIsHidden ] = useState(true);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -184,9 +195,22 @@ const ContextMenu = ({
|
||||
&& offsetTarget.offsetParent instanceof HTMLElement
|
||||
) {
|
||||
const { current: container } = containerRef;
|
||||
|
||||
// make sure the max height is not set
|
||||
// @ts-ignore
|
||||
container.style.maxHeight = null;
|
||||
const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
|
||||
const outerHeight = getComputedOuterHeight(container);
|
||||
const height = Math.min(MAX_HEIGHT, outerHeight);
|
||||
let outerHeight = getComputedOuterHeight(container);
|
||||
let height = Math.min(MAX_HEIGHT, outerHeight);
|
||||
|
||||
if (offsetTop + height > offsetHeight + scrollTop && height > offsetTop) {
|
||||
// top offset and + padding + border
|
||||
container.style.maxHeight = `${offsetTop - ((spacing[2] * 2) + 2)}px`;
|
||||
}
|
||||
|
||||
// get the height after style changes
|
||||
outerHeight = getComputedOuterHeight(container);
|
||||
height = Math.min(MAX_HEIGHT, outerHeight);
|
||||
|
||||
container.style.top = offsetTop + height > offsetHeight + scrollTop
|
||||
? `${offsetTop - outerHeight}`
|
||||
@@ -225,6 +249,7 @@ const ContextMenu = ({
|
||||
</Drawer>
|
||||
</JitsiPortal>
|
||||
: <div
|
||||
{ ...aria }
|
||||
aria-label = { accessibilityLabel }
|
||||
className = { cx(participantsPaneTheme.ignoredChildClassName,
|
||||
styles.contextMenu,
|
||||
@@ -237,7 +262,7 @@ const ContextMenu = ({
|
||||
onMouseEnter = { onMouseEnter }
|
||||
onMouseLeave = { onMouseLeave }
|
||||
ref = { containerRef }
|
||||
role = 'menu'
|
||||
role = { role ?? 'menu' }
|
||||
tabIndex = { tabIndex }>
|
||||
{children}
|
||||
</div>;
|
||||
|
||||
@@ -178,7 +178,14 @@ const ContextMenuItem = ({
|
||||
className = { styles.contextMenuItemIcon }
|
||||
size = { 20 }
|
||||
src = { icon } />}
|
||||
{text && <span className = { cx(styles.text, textClassName) }>{text}</span>}
|
||||
{text && (
|
||||
<span
|
||||
className = { cx(styles.text,
|
||||
_overflowDrawer && styles.drawerText,
|
||||
textClassName) }>
|
||||
{text}
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,134 +1,19 @@
|
||||
import React, { useCallback, useContext, useEffect } from 'react';
|
||||
import FocusLock from 'react-focus-lock';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { keyframes } from 'tss-react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { hideDialog } from '../../../dialog/actions';
|
||||
import { IconCloseLarge } from '../../../icons/svg';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
|
||||
import BaseDialog, { IProps as IBaseDialogProps } from './BaseDialog';
|
||||
import Button from './Button';
|
||||
import ClickableIcon from './ClickableIcon';
|
||||
import { DialogTransitionContext } from './DialogTransition';
|
||||
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'fixed',
|
||||
color: theme.palette.text01,
|
||||
...withPixelLineHeight(theme.typography.bodyLongRegular),
|
||||
top: 0,
|
||||
left: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
zIndex: 301,
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
`} 0.2s forwards ease-out`,
|
||||
|
||||
'&.unmount': {
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`} 0.15s forwards ease-in`
|
||||
}
|
||||
},
|
||||
|
||||
backdrop: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: theme.palette.ui02,
|
||||
opacity: 0.75
|
||||
},
|
||||
|
||||
modal: {
|
||||
backgroundColor: theme.palette.ui01,
|
||||
border: `1px solid ${theme.palette.ui03}`,
|
||||
boxShadow: '0px 4px 25px 4px rgba(20, 20, 20, 0.6)',
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 'auto',
|
||||
minHeight: '200px',
|
||||
maxHeight: '80vh',
|
||||
marginTop: '64px',
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
margin-top: 85px
|
||||
}
|
||||
100% {
|
||||
margin-top: 64px
|
||||
}
|
||||
`} 0.2s forwards ease-out`,
|
||||
|
||||
'&.medium': {
|
||||
width: '400px'
|
||||
},
|
||||
|
||||
'&.large': {
|
||||
width: '664px'
|
||||
},
|
||||
|
||||
'&.unmount': {
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
margin-top: 64px
|
||||
}
|
||||
100% {
|
||||
margin-top: 40px
|
||||
}
|
||||
`} 0.15s forwards ease-in`
|
||||
},
|
||||
|
||||
'@media (max-width: 448px)': {
|
||||
width: '100% !important',
|
||||
maxHeight: 'initial',
|
||||
height: '100%',
|
||||
margin: 0,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
margin-top: 15px
|
||||
}
|
||||
100% {
|
||||
margin-top: 0
|
||||
}
|
||||
`} 0.2s forwards ease-out`,
|
||||
|
||||
'&.unmount': {
|
||||
animation: `${keyframes`
|
||||
0% {
|
||||
margin-top: 0
|
||||
}
|
||||
100% {
|
||||
margin-top: 15px
|
||||
}
|
||||
`} 0.15s forwards ease-in`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
header: {
|
||||
width: '100%',
|
||||
padding: '24px',
|
||||
@@ -176,15 +61,11 @@ const useStyles = makeStyles()(theme => {
|
||||
'& button:last-child': {
|
||||
marginLeft: '16px'
|
||||
}
|
||||
},
|
||||
|
||||
focusLock: {
|
||||
zIndex: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
interface IDialogProps {
|
||||
interface IDialogProps extends IBaseDialogProps {
|
||||
back?: {
|
||||
hidden?: boolean;
|
||||
onClick?: () => void;
|
||||
@@ -195,11 +76,7 @@ interface IDialogProps {
|
||||
translationKey?: string;
|
||||
};
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
description?: string;
|
||||
disableAutoHideOnSubmit?: boolean;
|
||||
disableBackdropClose?: boolean;
|
||||
disableEnter?: boolean;
|
||||
hideCloseButton?: boolean;
|
||||
ok?: {
|
||||
disabled?: boolean;
|
||||
@@ -208,9 +85,6 @@ interface IDialogProps {
|
||||
};
|
||||
onCancel?: () => void;
|
||||
onSubmit?: () => void;
|
||||
size?: 'large' | 'medium';
|
||||
title?: string;
|
||||
titleKey?: string;
|
||||
}
|
||||
|
||||
const Dialog = ({
|
||||
@@ -226,13 +100,12 @@ const Dialog = ({
|
||||
ok = { translationKey: 'dialog.Ok' },
|
||||
onCancel,
|
||||
onSubmit,
|
||||
size = 'medium',
|
||||
size,
|
||||
title,
|
||||
titleKey
|
||||
}: IDialogProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const { isUnmounting } = useContext(DialogTransitionContext);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
@@ -245,81 +118,59 @@ const Dialog = ({
|
||||
onSubmit?.();
|
||||
}, [ onSubmit ]);
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
if (e.key === 'Enter' && !disableEnter) {
|
||||
submit();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onBackdropClick = useCallback(() => {
|
||||
!disableBackdropClose && onClose();
|
||||
}, [ disableBackdropClose, onClose ]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className = { cx(classes.container, isUnmounting && 'unmount') }>
|
||||
<BaseDialog
|
||||
className = { className }
|
||||
description = { description }
|
||||
disableBackdropClose = { disableBackdropClose }
|
||||
disableEnter = { disableEnter }
|
||||
onClose = { onClose }
|
||||
size = { size }
|
||||
submit = { submit }
|
||||
title = { title }
|
||||
titleKey = { titleKey }>
|
||||
<div className = { classes.header }>
|
||||
<p
|
||||
className = { classes.title }
|
||||
id = 'dialog-title'>
|
||||
{title ?? t(titleKey ?? '')}
|
||||
</p>
|
||||
{!hideCloseButton && (
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.close') }
|
||||
className = { classes.closeIcon }
|
||||
icon = { IconCloseLarge }
|
||||
id = 'modal-header-close-button'
|
||||
onClick = { onClose } />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className = { classes.backdrop }
|
||||
onClick = { onBackdropClick } />
|
||||
<FocusLock className = { classes.focusLock }>
|
||||
<div
|
||||
aria-describedby = { description }
|
||||
aria-labelledby = { title ?? t(titleKey ?? '') }
|
||||
aria-modal = { true }
|
||||
className = { cx(classes.modal, isUnmounting && 'unmount', size, className) }
|
||||
role = 'dialog'>
|
||||
<div className = { classes.header }>
|
||||
<p
|
||||
className = { classes.title }
|
||||
id = 'dialog-title'>
|
||||
{title ?? t(titleKey ?? '')}
|
||||
</p>
|
||||
{!hideCloseButton && (
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.close') }
|
||||
className = { classes.closeIcon }
|
||||
icon = { IconCloseLarge }
|
||||
id = 'modal-header-close-button'
|
||||
onClick = { onClose } />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className = { classes.content }
|
||||
data-autofocus-inside = 'true'>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className = { classes.footer }
|
||||
data-autofocus-inside = 'true'>
|
||||
{!back.hidden && <Button
|
||||
accessibilityLabel = { t(back.translationKey ?? '') }
|
||||
labelKey = { back.translationKey }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { back.onClick }
|
||||
type = 'secondary' />}
|
||||
{!cancel.hidden && <Button
|
||||
accessibilityLabel = { t(cancel.translationKey ?? '') }
|
||||
labelKey = { cancel.translationKey }
|
||||
onClick = { onClose }
|
||||
type = 'tertiary' />}
|
||||
{!ok.hidden && <Button
|
||||
accessibilityLabel = { t(ok.translationKey ?? '') }
|
||||
disabled = { ok.disabled }
|
||||
id = 'modal-dialog-ok-button'
|
||||
labelKey = { ok.translationKey }
|
||||
onClick = { submit } />}
|
||||
</div>
|
||||
</div>
|
||||
</FocusLock>
|
||||
</div>
|
||||
className = { classes.content }
|
||||
data-autofocus-inside = 'true'>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className = { classes.footer }
|
||||
data-autofocus-inside = 'true'>
|
||||
{!back.hidden && <Button
|
||||
accessibilityLabel = { t(back.translationKey ?? '') }
|
||||
labelKey = { back.translationKey }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { back.onClick }
|
||||
type = 'secondary' />}
|
||||
{!cancel.hidden && <Button
|
||||
accessibilityLabel = { t(cancel.translationKey ?? '') }
|
||||
labelKey = { cancel.translationKey }
|
||||
onClick = { onClose }
|
||||
type = 'tertiary' />}
|
||||
{!ok.hidden && <Button
|
||||
accessibilityLabel = { t(ok.translationKey ?? '') }
|
||||
disabled = { ok.disabled }
|
||||
id = 'modal-dialog-ok-button'
|
||||
labelKey = { ok.translationKey }
|
||||
onClick = { submit } />}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ModalTransition } from '@atlaskit/modal-dialog';
|
||||
import React, { Component, ComponentType } from 'react';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
@@ -21,11 +20,6 @@ interface IProps {
|
||||
*/
|
||||
_componentProps: Object;
|
||||
|
||||
/**
|
||||
* Whether the dialog is using the new component.
|
||||
*/
|
||||
_isNewDialog: boolean;
|
||||
|
||||
/**
|
||||
* Whether the overflow drawer should be used.
|
||||
*/
|
||||
@@ -74,16 +68,12 @@ class DialogContainer extends Component<IProps> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return this.props._isNewDialog ? (
|
||||
return (
|
||||
<DialogTransition>
|
||||
{this.props._overflowDrawer
|
||||
? <JitsiPortal>{this._renderDialogContent()}</JitsiPortal>
|
||||
: this._renderDialogContent() }
|
||||
: this._renderDialogContent()}
|
||||
</DialogTransition>
|
||||
) : (
|
||||
<ModalTransition>
|
||||
{ this._renderDialogContent() }
|
||||
</ModalTransition>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -104,7 +94,6 @@ function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_component: stateFeaturesBaseDialog.component,
|
||||
_componentProps: stateFeaturesBaseDialog.componentProps,
|
||||
_isNewDialog: stateFeaturesBaseDialog.isNewDialog,
|
||||
_overflowDrawer: overflowDrawer,
|
||||
_reducedUI: reducedUI
|
||||
};
|
||||
|
||||
335
react/features/base/ui/components/web/DialogWithTabs.tsx
Normal file
335
react/features/base/ui/components/web/DialogWithTabs.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { hideDialog } from '../../../dialog/actions';
|
||||
import { IconArrowBack, IconCloseLarge } from '../../../icons/svg';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
|
||||
import BaseDialog, { IProps as IBaseProps } from './BaseDialog';
|
||||
import Button from './Button';
|
||||
import ClickableIcon from './ClickableIcon';
|
||||
import ContextMenuItem from './ContextMenuItem';
|
||||
|
||||
const MOBILE_BREAKPOINT = 607;
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
dialog: {
|
||||
flexDirection: 'row',
|
||||
height: '560px',
|
||||
|
||||
'@media (min-width: 608px) and (max-width: 712px)': {
|
||||
width: '560px'
|
||||
},
|
||||
|
||||
[`@media (max-width: ${MOBILE_BREAKPOINT}px)`]: {
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0
|
||||
},
|
||||
|
||||
'@media (max-width: 448px)': {
|
||||
height: '100%'
|
||||
}
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: '211px',
|
||||
maxWidth: '100%',
|
||||
borderRight: `1px solid ${theme.palette.ui03}`,
|
||||
|
||||
[`@media (max-width: ${MOBILE_BREAKPOINT}px)`]: {
|
||||
width: '100%',
|
||||
borderRight: 'none'
|
||||
}
|
||||
},
|
||||
|
||||
menuItemMobile: {
|
||||
paddingLeft: '24px'
|
||||
},
|
||||
|
||||
titleContainer: {
|
||||
margin: 0,
|
||||
padding: '24px',
|
||||
paddingRight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
[`@media (max-width: ${MOBILE_BREAKPOINT}px)`]: {
|
||||
padding: '16px 24px'
|
||||
}
|
||||
},
|
||||
|
||||
title: {
|
||||
...withPixelLineHeight(theme.typography.heading5),
|
||||
color: `${theme.palette.text01} !important`,
|
||||
margin: 0,
|
||||
padding: 0
|
||||
},
|
||||
|
||||
contentContainer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
padding: '24px',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
|
||||
[`@media (max-width: ${MOBILE_BREAKPOINT}px)`]: {
|
||||
padding: '0'
|
||||
}
|
||||
},
|
||||
|
||||
buttonContainer: {
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
flexGrow: 0,
|
||||
|
||||
[`@media (max-width: ${MOBILE_BREAKPOINT}px)`]: {
|
||||
justifyContent: 'space-between',
|
||||
padding: '16px 24px'
|
||||
}
|
||||
},
|
||||
|
||||
backContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
'& > button': {
|
||||
marginRight: '24px'
|
||||
}
|
||||
},
|
||||
|
||||
closeIcon: {
|
||||
'&:focus': {
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
|
||||
[`@media (max-width: ${MOBILE_BREAKPOINT}px)`]: {
|
||||
padding: '0 24px'
|
||||
}
|
||||
},
|
||||
|
||||
footer: {
|
||||
justifyContent: 'flex-end',
|
||||
|
||||
'& button:last-child': {
|
||||
marginLeft: '16px'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
interface IObject {
|
||||
[key: string]: string | string[] | boolean | number | number[] | {} | undefined;
|
||||
}
|
||||
|
||||
export interface IDialogTab {
|
||||
className?: string;
|
||||
component: ComponentType<any>;
|
||||
icon: Function;
|
||||
labelKey: string;
|
||||
name: string;
|
||||
props?: IObject;
|
||||
propsUpdateFunction?: (tabState: IObject, newProps: IObject) => IObject;
|
||||
submit?: Function;
|
||||
}
|
||||
|
||||
interface IProps extends IBaseProps {
|
||||
defaultTab?: string;
|
||||
tabs: IDialogTab[];
|
||||
}
|
||||
|
||||
const DialogWithTabs = ({
|
||||
className,
|
||||
defaultTab,
|
||||
titleKey,
|
||||
tabs
|
||||
}: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [ selectedTab, setSelectedTab ] = useState<string | undefined>(defaultTab ?? tabs[0].name);
|
||||
const [ tabStates, setTabStates ] = useState(tabs.map(tab => tab.props));
|
||||
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
|
||||
const [ isMobile, setIsMobile ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (clientWidth <= MOBILE_BREAKPOINT) {
|
||||
!isMobile && setIsMobile(true);
|
||||
} else {
|
||||
isMobile && setIsMobile(false);
|
||||
}
|
||||
}, [ clientWidth, isMobile ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setSelectedTab(undefined);
|
||||
} else {
|
||||
setSelectedTab(defaultTab ?? tabs[0].name);
|
||||
}
|
||||
}, [ isMobile ]);
|
||||
|
||||
const back = useCallback(() => {
|
||||
setSelectedTab(undefined);
|
||||
}, []);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(hideDialog());
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback((tabName: string) => () => {
|
||||
setSelectedTab(tabName);
|
||||
}, []);
|
||||
|
||||
const getTabProps = (tabId: number) => {
|
||||
const tabConfiguration = tabs[tabId];
|
||||
const currentTabState = tabStates[tabId];
|
||||
|
||||
if (tabConfiguration.propsUpdateFunction) {
|
||||
return tabConfiguration.propsUpdateFunction(
|
||||
currentTabState ?? {},
|
||||
tabConfiguration.props ?? {});
|
||||
}
|
||||
|
||||
return { ...currentTabState };
|
||||
};
|
||||
|
||||
const onTabStateChange = useCallback((tabId: number, state: IObject) => {
|
||||
const newTabStates = [ ...tabStates ];
|
||||
|
||||
newTabStates[tabId] = state;
|
||||
setTabStates(newTabStates);
|
||||
}, [ tabStates ]);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
tabs.forEach(({ submit }, idx) => {
|
||||
submit?.(tabStates[idx]);
|
||||
});
|
||||
onClose();
|
||||
}, [ tabs, tabStates ]);
|
||||
|
||||
const selectedTabIndex = useMemo(() => {
|
||||
if (selectedTab) {
|
||||
return tabs.findIndex(tab => tab.name === selectedTab);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [ selectedTab ]);
|
||||
|
||||
const selectedTabComponent = useMemo(() => {
|
||||
if (selectedTabIndex !== null) {
|
||||
const TabComponent = tabs[selectedTabIndex].component;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { tabs[selectedTabIndex].className }
|
||||
key = { tabs[selectedTabIndex].name }>
|
||||
<TabComponent
|
||||
onTabStateChange = { onTabStateChange }
|
||||
tabId = { selectedTabIndex }
|
||||
{ ...getTabProps(selectedTabIndex) } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [ selectedTabIndex, tabStates ]);
|
||||
|
||||
const closeIcon = useMemo(() => (
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.close') }
|
||||
className = { classes.closeIcon }
|
||||
icon = { IconCloseLarge }
|
||||
id = 'modal-header-close-button'
|
||||
onClick = { onClose } />
|
||||
), [ onClose ]);
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className = { cx(classes.dialog, className) }
|
||||
onClose = { onClose }
|
||||
size = 'large'>
|
||||
{(!isMobile || !selectedTab) && (
|
||||
<div className = { classes.sidebar }>
|
||||
<div className = { classes.titleContainer }>
|
||||
<h2 className = { classes.title }>{t(titleKey ?? '')}</h2>
|
||||
{isMobile && closeIcon}
|
||||
</div>
|
||||
{tabs.map(tab => {
|
||||
const label = t(tab.labelKey);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { label }
|
||||
className = { cx(isMobile && classes.menuItemMobile) }
|
||||
icon = { tab.icon }
|
||||
key = { tab.name }
|
||||
onClick = { onClick(tab.name) }
|
||||
selected = { tab.name === selectedTab }
|
||||
text = { label } />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{(!isMobile || selectedTab) && (
|
||||
<div className = { classes.contentContainer }>
|
||||
<div className = { classes.buttonContainer }>
|
||||
{isMobile && (
|
||||
<span className = { classes.backContainer }>
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.Back') }
|
||||
className = { classes.closeIcon }
|
||||
icon = { IconArrowBack }
|
||||
id = 'modal-header-back-button'
|
||||
onClick = { back } />
|
||||
<h2 className = { classes.title }>
|
||||
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
|
||||
</h2>
|
||||
</span>
|
||||
)}
|
||||
{closeIcon}
|
||||
</div>
|
||||
<div className = { classes.content }>
|
||||
{selectedTabComponent}
|
||||
</div>
|
||||
<div
|
||||
className = { cx(classes.buttonContainer, classes.footer) }>
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.Cancel') }
|
||||
id = 'modal-dialog-cancel-button'
|
||||
labelKey = { 'dialog.Cancel' }
|
||||
onClick = { onClose }
|
||||
type = 'tertiary' />
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.Ok') }
|
||||
id = 'modal-dialog-ok-button'
|
||||
labelKey = { 'dialog.Ok' }
|
||||
onClick = { onSubmit } />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogWithTabs;
|
||||
@@ -23,6 +23,7 @@ interface IProps extends IInputProps {
|
||||
onKeyPress?: (e: React.KeyboardEvent) => void;
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
testId?: string;
|
||||
textarea?: boolean;
|
||||
type?: 'text' | 'email' | 'number' | 'password';
|
||||
}
|
||||
@@ -152,6 +153,7 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
placeholder,
|
||||
readOnly = false,
|
||||
required,
|
||||
testId,
|
||||
textarea = false,
|
||||
type = 'text',
|
||||
value
|
||||
@@ -201,6 +203,7 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
autoFocus = { autoFocus }
|
||||
className = { cx(styles.input, isMobile && 'is-mobile',
|
||||
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
|
||||
data-testid = { testId }
|
||||
disabled = { disabled }
|
||||
{ ...(id ? { id } : {}) }
|
||||
maxLength = { maxLength }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
|
||||
interface ITabProps {
|
||||
accessibilityLabel: string;
|
||||
className?: string;
|
||||
onChange: (id: string) => void;
|
||||
selected: string;
|
||||
tabs: Array<{
|
||||
@@ -78,10 +79,11 @@ const useStyles = makeStyles()(theme => {
|
||||
|
||||
|
||||
const Tabs = ({
|
||||
tabs,
|
||||
accessibilityLabel,
|
||||
className,
|
||||
onChange,
|
||||
selected,
|
||||
accessibilityLabel
|
||||
tabs
|
||||
}: ITabProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const isMobile = isMobileBrowser();
|
||||
@@ -93,7 +95,7 @@ const Tabs = ({
|
||||
return (
|
||||
<div
|
||||
aria-label = { accessibilityLabel }
|
||||
className = { classes.container }
|
||||
className = { cx(classes.container, className) }
|
||||
role = 'tablist'>
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
|
||||
@@ -13,6 +13,8 @@ export enum BUTTON_TYPES {
|
||||
*/
|
||||
export const BUTTON_MODES: {
|
||||
CONTAINED: 'contained';
|
||||
TEXT: 'text';
|
||||
} = {
|
||||
CONTAINED: 'contained'
|
||||
CONTAINED: 'contained',
|
||||
TEXT: 'text'
|
||||
};
|
||||
|
||||
@@ -281,22 +281,3 @@ export const commonStyles = (theme: Theme) => {
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the global styles.
|
||||
*
|
||||
* @param {Object} theme - The Jitsi theme.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const getGlobalStyles = (theme: Theme) => {
|
||||
return {
|
||||
// @atlaskit/modal-dialog OVERRIDES
|
||||
'.atlaskit-portal div[role=dialog]': {
|
||||
// override dialog background
|
||||
'& > div': {
|
||||
background: theme.palette.ui02,
|
||||
color: theme.palette.text01
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/* eslint-disable lines-around-comment, max-len */
|
||||
|
||||
import { navigate }
|
||||
// @ts-ignore
|
||||
from '../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
// @ts-ignore
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
|
||||
import { OPEN_CHAT } from './actionTypes';
|
||||
|
||||
export * from './actions.any';
|
||||
@@ -6,13 +14,19 @@ export * from './actions.any';
|
||||
* Displays the chat panel.
|
||||
*
|
||||
* @param {Object} participant - The recipient for the private chat.
|
||||
* @param {boolean} disablePolls - Checks if polls are disabled.
|
||||
*
|
||||
* @returns {{
|
||||
* participant: Participant,
|
||||
* participant: participant,
|
||||
* type: OPEN_CHAT
|
||||
* }}
|
||||
*/
|
||||
export function openChat(participant: Object) {
|
||||
export function openChat(participant: Object, disablePolls: boolean) {
|
||||
if (disablePolls) {
|
||||
navigate(screen.conference.chat);
|
||||
}
|
||||
navigate(screen.conference.chatandpolls.main);
|
||||
|
||||
return {
|
||||
participant,
|
||||
type: OPEN_CHAT
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import { IStore } from '../app/types';
|
||||
import { getParticipantById } from '../base/participants/functions';
|
||||
|
||||
import { OPEN_CHAT } from './actionTypes';
|
||||
import { closeChat } from './actions.any';
|
||||
@@ -26,27 +25,6 @@ export function openChat(participant?: Object) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the chat panel for a participant identified by an id.
|
||||
*
|
||||
* @param {string} id - The id of the participant.
|
||||
* @returns {{
|
||||
* participant: Participant,
|
||||
* type: OPEN_CHAT
|
||||
* }}
|
||||
*/
|
||||
export function openChatById(id: string) {
|
||||
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const participant = getParticipantById(getState(), id);
|
||||
|
||||
return dispatch({
|
||||
participant,
|
||||
type: OPEN_CHAT
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Toggles display of the chat panel.
|
||||
*
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconMessage, IconReply } from '../../../base/icons';
|
||||
import { getParticipantById } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
import { handleLobbyChatInitialized, openChat } from '../../../chat/actions';
|
||||
import { handleLobbyChatInitialized, openChat } from '../../../chat/actions.native';
|
||||
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import { CalleeInfoContainer } from '../../../invite';
|
||||
import { LargeVideo } from '../../../large-video';
|
||||
import { startKnocking } from '../../../lobby/actions.any';
|
||||
import { KnockingParticipantList } from '../../../lobby/components/native';
|
||||
import { getIsLobbyVisible } from '../../../lobby/functions';
|
||||
import { navigate }
|
||||
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
@@ -433,7 +432,13 @@ class Conference extends AbstractConference<Props, State> {
|
||||
|
||||
<LonelyMeetingExperience />
|
||||
|
||||
{ _shouldDisplayTileView || <><Filmstrip /><Toolbox /></> }
|
||||
{
|
||||
_shouldDisplayTileView
|
||||
|| <>
|
||||
<Filmstrip />
|
||||
<Toolbox />
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
|
||||
<SafeAreaView
|
||||
@@ -463,10 +468,10 @@ class Conference extends AbstractConference<Props, State> {
|
||||
<AlwaysOnLabels createOnPress = { this._createOnPress } />
|
||||
</View>
|
||||
{ this._renderNotificationsContainer() }
|
||||
<KnockingParticipantList />
|
||||
</SafeAreaView>
|
||||
|
||||
<TestConnectionInfo />
|
||||
|
||||
{ this._renderConferenceNotification() }
|
||||
|
||||
{_shouldDisplayTileView && <Toolbox />}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { CalleeInfoContainer } from '../../../invite';
|
||||
import { LargeVideo } from '../../../large-video';
|
||||
import { LobbyScreen } from '../../../lobby';
|
||||
import { getIsLobbyVisible } from '../../../lobby/functions';
|
||||
import { getOverlayToRender } from '../../../overlay/functions.web';
|
||||
import { ParticipantsPane } from '../../../participants-pane/components/web';
|
||||
import Prejoin from '../../../prejoin/components/web/Prejoin';
|
||||
import { isPrejoinPageVisible } from '../../../prejoin/functions';
|
||||
@@ -33,6 +34,7 @@ import type { AbstractProps } from '../AbstractConference';
|
||||
import ConferenceInfo from './ConferenceInfo';
|
||||
import { default as Notice } from './Notice';
|
||||
|
||||
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
@@ -59,6 +61,11 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
_backgroundAlpha: number,
|
||||
|
||||
/**
|
||||
* Are any overlays visible?
|
||||
*/
|
||||
_isAnyOverlayVisible: boolean,
|
||||
|
||||
/**
|
||||
* The CSS class to apply to the root of {@link Conference} to modify the
|
||||
* application layout.
|
||||
@@ -198,6 +205,7 @@ class Conference extends AbstractConference<Props, *> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_isAnyOverlayVisible,
|
||||
_layoutClassName,
|
||||
_notificationsVisible,
|
||||
_overflowDrawer,
|
||||
@@ -234,7 +242,7 @@ class Conference extends AbstractConference<Props, *> {
|
||||
|
||||
{ _showPrejoin || _showLobby || <Toolbox /> }
|
||||
|
||||
{_notificationsVisible && (_overflowDrawer
|
||||
{_notificationsVisible && !_isAnyOverlayVisible && (_overflowDrawer
|
||||
? <JitsiPortal className = 'notification-portal'>
|
||||
{this.renderNotificationsContainer({ portal: true })}
|
||||
</JitsiPortal>
|
||||
@@ -383,6 +391,7 @@ function _mapStateToProps(state) {
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_backgroundAlpha: backgroundAlpha,
|
||||
_isAnyOverlayVisible: Boolean(getOverlayToRender(state)),
|
||||
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
|
||||
_mouseMoveCallbackInterval: mouseMoveCallbackInterval,
|
||||
_overflowDrawer: overflowDrawer,
|
||||
|
||||
@@ -9,7 +9,7 @@ import Label from '../../../base/label/components/web/Label';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { Tooltip } from '../../../base/tooltip';
|
||||
import { open as openParticipantsPane } from '../../../participants-pane/actions';
|
||||
import { open as openParticipantsPane } from '../../../participants-pane/actions.web';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { areThereNotifications } from '../notifications/functions';
|
||||
import { getOverlayToRender } from '../overlay/functions';
|
||||
|
||||
|
||||
/**
|
||||
* Tells whether or not the notifications should be displayed within
|
||||
@@ -12,10 +12,7 @@ import { getOverlayToRender } from '../overlay/functions';
|
||||
*/
|
||||
export function shouldDisplayNotifications(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const isAnyOverlayVisible = Boolean(getOverlayToRender(state));
|
||||
const { calleeInfoVisible } = state['features/invite'];
|
||||
|
||||
return areThereNotifications(state)
|
||||
&& !isAnyOverlayVisible
|
||||
&& !calleeInfoVisible;
|
||||
return areThereNotifications(state) && !calleeInfoVisible;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @flow
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { appNavigate } from '../app/actions';
|
||||
@@ -1,192 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React, { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { createDeepLinkingPageEvent, sendAnalytics } from '../../analytics';
|
||||
import { IDeeplinkingConfig } from '../../base/config/configType';
|
||||
import { isSupportedBrowser } from '../../base/environment';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
import Button from '../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../base/ui/constants.web';
|
||||
import {
|
||||
openDesktopApp,
|
||||
openWebApp
|
||||
} from '../actions';
|
||||
import { _TNS } from '../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link DeepLinkingDesktopPage}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The deeplinking config.
|
||||
*/
|
||||
_deeplinkingCfg: IDeeplinkingConfig,
|
||||
|
||||
/**
|
||||
* Used to dispatch actions from the buttons.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Used to obtain translations.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* React component representing the deep linking page.
|
||||
*
|
||||
* @class DeepLinkingDesktopPage
|
||||
*/
|
||||
class DeepLinkingDesktopPage<P : Props> extends Component<P> {
|
||||
/**
|
||||
* Initializes a new {@code DeepLinkingDesktopPage} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onLaunchWeb = this._onLaunchWeb.bind(this);
|
||||
this._onTryAgain = this._onTryAgain.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Component's componentDidMount method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'displayed', 'DeepLinkingDesktop', { isMobileBrowser: false }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t, _deeplinkingCfg: { desktop = {}, hideLogo, showImage } } = this.props;
|
||||
const { appName } = desktop;
|
||||
const rightColumnStyle
|
||||
= showImage ? null : { width: '100%' };
|
||||
|
||||
return (
|
||||
|
||||
// Enabling light theme because of the color of the buttons.
|
||||
<AtlasKitThemeProvider mode = 'light'>
|
||||
<div className = 'deep-linking-desktop'>
|
||||
<div className = 'header'>
|
||||
{
|
||||
hideLogo
|
||||
? null
|
||||
: <img
|
||||
alt = { t('welcomepage.logo.logoDeepLinking') }
|
||||
className = 'logo'
|
||||
src = 'images/logo-deep-linking.png' />
|
||||
}
|
||||
</div>
|
||||
<div className = 'content'>
|
||||
{
|
||||
showImage
|
||||
? <div className = 'leftColumn'>
|
||||
<div className = 'leftColumnContent'>
|
||||
<div className = 'image' />
|
||||
</div>
|
||||
</div> : null
|
||||
}
|
||||
<div
|
||||
className = 'rightColumn'
|
||||
style = { rightColumnStyle }>
|
||||
<div className = 'rightColumnContent'>
|
||||
<h1 className = 'title'>
|
||||
{
|
||||
t(`${_TNS}.title`,
|
||||
{ app: appName })
|
||||
}
|
||||
</h1>
|
||||
<p className = 'description'>
|
||||
{
|
||||
t(
|
||||
`${_TNS}.${isSupportedBrowser()
|
||||
? 'description'
|
||||
: 'descriptionWithoutWeb'}`,
|
||||
{ app: appName }
|
||||
)
|
||||
}
|
||||
</p>
|
||||
<div className = 'buttons'>
|
||||
<Button
|
||||
label = { t(`${_TNS}.tryAgainButton`) }
|
||||
onClick = { this._onTryAgain }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
{
|
||||
isSupportedBrowser()
|
||||
&& <Button
|
||||
label = { t(`${_TNS}.launchWebButton`) }
|
||||
onClick = { this._onLaunchWeb }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AtlasKitThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
_onTryAgain: () => void;
|
||||
|
||||
/**
|
||||
* Handles try again button clicks.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTryAgain() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'tryAgainButton', { isMobileBrowser: false }));
|
||||
this.props.dispatch(openDesktopApp());
|
||||
}
|
||||
|
||||
_onLaunchWeb: () => void;
|
||||
|
||||
/**
|
||||
* Handles launch web button clicks.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLaunchWeb() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'launchWebButton', { isMobileBrowser: false }));
|
||||
this.props.dispatch(openWebApp());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code DeepLinkingDesktopPage} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_deeplinkingCfg: state['features/base/config'].deeplinking || {}
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(DeepLinkingDesktopPage));
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { createDeepLinkingPageEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { IDeeplinkingConfig } from '../../base/config/configType';
|
||||
import { getLegalUrls } from '../../base/config/functions.any';
|
||||
import { isSupportedBrowser } from '../../base/environment/environment';
|
||||
import { translate, translateToHTML } from '../../base/i18n/functions';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import Button from '../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../base/ui/constants.any';
|
||||
import {
|
||||
openDesktopApp,
|
||||
openWebApp
|
||||
} from '../actions';
|
||||
import { _TNS } from '../constants';
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
background: '#1E1E1E',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex'
|
||||
},
|
||||
contentPane: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: theme.palette.ui01,
|
||||
border: `1px solid ${theme.palette.ui03}`,
|
||||
padding: 40,
|
||||
borderRadius: 16,
|
||||
maxWidth: 410,
|
||||
color: theme.palette.text01
|
||||
},
|
||||
logo: {
|
||||
marginBottom: 32
|
||||
},
|
||||
launchingMeetingLabel: {
|
||||
marginBottom: 16,
|
||||
...withPixelLineHeight(theme.typography.heading4)
|
||||
},
|
||||
roomName: {
|
||||
marginBottom: 32,
|
||||
...withPixelLineHeight(theme.typography.heading5)
|
||||
},
|
||||
descriptionLabel: {
|
||||
marginBottom: 32,
|
||||
...withPixelLineHeight(theme.typography.bodyLongRegular)
|
||||
},
|
||||
buttonsContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
'& > *:not(:last-child)': {
|
||||
marginRight: 16
|
||||
}
|
||||
},
|
||||
separator: {
|
||||
marginTop: 40,
|
||||
height: 1,
|
||||
maxWidth: 390,
|
||||
background: theme.palette.ui03
|
||||
},
|
||||
label: {
|
||||
marginTop: 40,
|
||||
...withPixelLineHeight(theme.typography.labelRegular),
|
||||
color: theme.palette.text02,
|
||||
'& a': {
|
||||
color: theme.palette.link01
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const DeepLinkingDesktopPage: React.FC<WithTranslation> = ({ t }) => {
|
||||
const dispatch = useDispatch();
|
||||
const room = useSelector((state: IReduxState) => decodeURIComponent(state['features/base/conference'].room || ''));
|
||||
const deeplinkingCfg = useSelector((state: IReduxState) =>
|
||||
state['features/base/config']?.deeplinking || {} as IDeeplinkingConfig);
|
||||
|
||||
const legalUrls = useSelector(getLegalUrls);
|
||||
|
||||
const { hideLogo, desktop } = deeplinkingCfg;
|
||||
|
||||
const { classes: styles } = useStyles();
|
||||
const onLaunchWeb = useCallback(() => {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'launchWebButton', { isMobileBrowser: false }));
|
||||
dispatch(openWebApp());
|
||||
}, []);
|
||||
const onTryAgain = useCallback(() => {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'tryAgainButton', { isMobileBrowser: false }));
|
||||
dispatch(openDesktopApp());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'displayed', 'DeepLinkingDesktop', { isMobileBrowser: false }));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className = { styles.container }>
|
||||
<div className = { styles.contentPane }>
|
||||
<div className = 'header'>
|
||||
{
|
||||
!hideLogo
|
||||
&& <img
|
||||
alt = { t('welcomepage.logo.logoDeepLinking') }
|
||||
className = { styles.logo }
|
||||
src = 'images/logo-deep-linking.png' />
|
||||
}
|
||||
</div>
|
||||
<div className = { styles.launchingMeetingLabel }>
|
||||
{
|
||||
t(`${_TNS}.titleNew`)
|
||||
}
|
||||
</div>
|
||||
<div className = { styles.roomName }>{ room }</div>
|
||||
<div className = { styles.descriptionLabel }>
|
||||
{
|
||||
isSupportedBrowser()
|
||||
? translateToHTML(t, `${_TNS}.descriptionNew`, { app: desktop?.appName })
|
||||
: t(`${_TNS}.descriptionWithoutWeb`, { app: desktop?.appName })
|
||||
}
|
||||
</div>
|
||||
<div className = { styles.buttonsContainer }>
|
||||
<Button
|
||||
label = { t(`${_TNS}.tryAgainButton`) }
|
||||
onClick = { onTryAgain } />
|
||||
{ isSupportedBrowser() && (
|
||||
<Button
|
||||
label = { t(`${_TNS}.launchWebButton`) }
|
||||
onClick = { onLaunchWeb }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className = { styles.separator } />
|
||||
<div className = { styles.label }> {translateToHTML(t, 'deepLinking.termsAndConditions', {
|
||||
termsAndConditionsLink: legalUrls.terms
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(DeepLinkingDesktopPage);
|
||||
@@ -1,299 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { createDeepLinkingPageEvent, sendAnalytics } from '../../analytics';
|
||||
import { IDeeplinkingConfig, IDeeplinkingMobileConfig } from '../../base/config/configType';
|
||||
import { isSupportedMobileBrowser } from '../../base/environment';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Platform } from '../../base/react';
|
||||
import { connect } from '../../base/redux';
|
||||
import { DialInSummary } from '../../invite';
|
||||
import { openWebApp } from '../actions';
|
||||
import { _TNS } from '../constants';
|
||||
import { generateDeepLinkingURL } from '../functions';
|
||||
import { renderPromotionalFooter } from '../renderPromotionalFooter';
|
||||
|
||||
/**
|
||||
* The namespace of the CSS styles of DeepLinkingMobilePage.
|
||||
*
|
||||
* @private
|
||||
* @type {string}
|
||||
*/
|
||||
const _SNS = 'deep-linking-mobile';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link DeepLinkingMobilePage}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The deeplinking config.
|
||||
*/
|
||||
_deeplinkingCfg: IDeeplinkingConfig,
|
||||
|
||||
/**
|
||||
* Application mobile deeplinking config.
|
||||
*/
|
||||
_mobileConfig: IDeeplinkingMobileConfig,
|
||||
|
||||
/**
|
||||
* The deeplinking url.
|
||||
*/
|
||||
_deepLinkingUrl: string,
|
||||
|
||||
/**
|
||||
* The name of the conference attempting to being joined.
|
||||
*/
|
||||
_room: string,
|
||||
|
||||
/**
|
||||
* The page current url.
|
||||
*/
|
||||
_url: URL,
|
||||
|
||||
/**
|
||||
* Used to dispatch actions from the buttons.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* The function to translate human-readable text.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* React component representing mobile browser page.
|
||||
*
|
||||
* @class DeepLinkingMobilePage
|
||||
*/
|
||||
class DeepLinkingMobilePage extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code DeepLinkingMobilePage} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onDownloadApp = this._onDownloadApp.bind(this);
|
||||
this._onLaunchWeb = this._onLaunchWeb.bind(this);
|
||||
this._onOpenApp = this._onOpenApp.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Component's componentDidMount method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'displayed', 'DeepLinkingMobile', { isMobileBrowser: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_deeplinkingCfg: { hideLogo },
|
||||
_mobileConfig: { downloadLink, appName },
|
||||
_room,
|
||||
t,
|
||||
_url,
|
||||
_deepLinkingUrl
|
||||
} = this.props;
|
||||
const downloadButtonClassName
|
||||
= `${_SNS}__button ${_SNS}__button_primary`;
|
||||
|
||||
|
||||
const onOpenLinkProperties = downloadLink
|
||||
? {
|
||||
// When opening a link to the download page, we want to let the
|
||||
// OS itself handle intercepting and opening the appropriate
|
||||
// app store. This avoids potential issues with browsers, such
|
||||
// as iOS Chrome, not opening the store properly.
|
||||
}
|
||||
: {
|
||||
// When falling back to another URL (Firebase) let the page be
|
||||
// opened in a new window. This helps prevent the user getting
|
||||
// trapped in an app-open-cycle where going back to the mobile
|
||||
// browser re-triggers the app-open behavior.
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className = { _SNS }>
|
||||
<div className = 'header'>
|
||||
{
|
||||
hideLogo
|
||||
? null
|
||||
: <img
|
||||
alt = { t('welcomepage.logo.logoDeepLinking') }
|
||||
className = 'logo'
|
||||
src = 'images/logo-deep-linking.png' />
|
||||
}
|
||||
</div>
|
||||
<div className = { `${_SNS}__body` }>
|
||||
<p className = { `${_SNS}__text` }>
|
||||
{ t(`${_TNS}.appNotInstalled`, { app: appName }) }
|
||||
</p>
|
||||
<p className = { `${_SNS}__text` }>
|
||||
{ t(`${_TNS}.ifHaveApp`) }
|
||||
</p>
|
||||
<a
|
||||
{ ...onOpenLinkProperties }
|
||||
className = { `${_SNS}__href` }
|
||||
href = { _deepLinkingUrl }
|
||||
onClick = { this._onOpenApp }
|
||||
target = '_top'>
|
||||
<button className = { `${_SNS}__button ${_SNS}__button_primary` }>
|
||||
{ t(`${_TNS}.joinInApp`) }
|
||||
</button>
|
||||
</a>
|
||||
<p className = { `${_SNS}__text` }>
|
||||
{ t(`${_TNS}.ifDoNotHaveApp`) }
|
||||
</p>
|
||||
<a
|
||||
{ ...onOpenLinkProperties }
|
||||
href = { this._generateDownloadURL() }
|
||||
onClick = { this._onDownloadApp }
|
||||
target = '_top'>
|
||||
<button className = { downloadButtonClassName }>
|
||||
{ t(`${_TNS}.downloadApp`) }
|
||||
</button>
|
||||
</a>
|
||||
{
|
||||
isSupportedMobileBrowser()
|
||||
? (
|
||||
<a
|
||||
onClick = { this._onLaunchWeb }
|
||||
target = '_top'>
|
||||
<button className = { downloadButtonClassName }>
|
||||
{ t(`${_TNS}.launchWebButton`) }
|
||||
</button>
|
||||
</a>
|
||||
) : (
|
||||
<b>
|
||||
{ t(`${_TNS}.unsupportedBrowser`) }
|
||||
</b>
|
||||
)
|
||||
}
|
||||
{ renderPromotionalFooter() }
|
||||
<DialInSummary
|
||||
className = 'deep-linking-dial-in'
|
||||
clickableNumbers = { true }
|
||||
room = { _room }
|
||||
url = { _url } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the URL for downloading the app.
|
||||
*
|
||||
* @private
|
||||
* @returns {string} - The URL for downloading the app.
|
||||
*/
|
||||
_generateDownloadURL() {
|
||||
const { _mobileConfig: { downloadLink, dynamicLink, appScheme } } = this.props;
|
||||
|
||||
if (downloadLink && typeof dynamicLink === 'undefined') {
|
||||
return downloadLink;
|
||||
}
|
||||
|
||||
const {
|
||||
apn,
|
||||
appCode,
|
||||
customDomain,
|
||||
ibi,
|
||||
isi
|
||||
} = dynamicLink || {};
|
||||
|
||||
const domain = customDomain ?? `https://${appCode}.app.goo.gl`;
|
||||
|
||||
return `${domain}/?link=${
|
||||
encodeURIComponent(window.location.href)}&apn=${
|
||||
apn}&ibi=${
|
||||
ibi}&isi=${
|
||||
isi}&ius=${
|
||||
appScheme}&efr=1`;
|
||||
}
|
||||
|
||||
_onDownloadApp: () => void;
|
||||
|
||||
/**
|
||||
* Handles download app button clicks.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDownloadApp() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'downloadAppButton', { isMobileBrowser: true }));
|
||||
}
|
||||
|
||||
_onLaunchWeb: () => void;
|
||||
|
||||
/**
|
||||
* Handles launch web button clicks.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLaunchWeb() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'launchWebButton', { isMobileBrowser: true }));
|
||||
this.props.dispatch(openWebApp());
|
||||
}
|
||||
|
||||
_onOpenApp: () => void;
|
||||
|
||||
/**
|
||||
* Handles open app button clicks.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenApp() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'openAppButton', { isMobileBrowser: true }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code DeepLinkingMobilePage} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { locationURL = {} } = state['features/base/connection'];
|
||||
const { deeplinking } = state['features/base/config'];
|
||||
const mobileConfig = deeplinking?.[Platform.OS] || {};
|
||||
|
||||
return {
|
||||
_deeplinkingCfg: deeplinking || {},
|
||||
_mobileConfig: mobileConfig,
|
||||
_room: decodeURIComponent(state['features/base/conference'].room),
|
||||
_url: locationURL,
|
||||
_deepLinkingUrl: generateDeepLinkingURL(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(DeepLinkingMobilePage));
|
||||
@@ -0,0 +1,241 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { createDeepLinkingPageEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { IDeeplinkingConfig, IDeeplinkingMobileConfig } from '../../base/config/configType';
|
||||
import { isSupportedMobileBrowser } from '../../base/environment/environment';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Platform from '../../base/react/Platform.web';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import Button from '../../base/ui/components/web/Button';
|
||||
// @ts-ignore
|
||||
import DialInSummary from '../../invite/components/dial-in-summary/web/DialInSummary';
|
||||
import { openWebApp } from '../actions';
|
||||
// @ts-ignore
|
||||
import { _TNS } from '../constants';
|
||||
// @ts-ignore
|
||||
import { generateDeepLinkingURL } from '../functions';
|
||||
|
||||
|
||||
const PADDINGS = {
|
||||
topBottom: 24,
|
||||
leftRight: 40
|
||||
};
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
background: '#1E1E1E',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
'& a': {
|
||||
textDecoration: 'none'
|
||||
}
|
||||
},
|
||||
contentPane: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
padding: `${PADDINGS.topBottom}px ${PADDINGS.leftRight}px`,
|
||||
maxWidth: 410,
|
||||
color: theme.palette.text01
|
||||
},
|
||||
launchingMeetingLabel: {
|
||||
marginTop: 24,
|
||||
textAlign: 'center',
|
||||
marginBottom: 32,
|
||||
...withPixelLineHeight(theme.typography.heading5)
|
||||
},
|
||||
roomNameLabel: {
|
||||
...withPixelLineHeight(theme.typography.bodyLongRegularLarge)
|
||||
},
|
||||
joinMeetWrapper: {
|
||||
marginTop: 24,
|
||||
width: '100%'
|
||||
},
|
||||
labelDescription: {
|
||||
textAlign: 'center',
|
||||
marginTop: 16,
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
|
||||
},
|
||||
linkWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
width: '100%'
|
||||
},
|
||||
linkLabel: {
|
||||
color: theme.palette.link01,
|
||||
...withPixelLineHeight(theme.typography.bodyLongBoldLarge)
|
||||
},
|
||||
supportedBrowserContent: {
|
||||
marginTop: 16,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
labelOr: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
|
||||
},
|
||||
separator: {
|
||||
marginTop: '32px',
|
||||
height: 1,
|
||||
width: `calc(100% + ${2 * PADDINGS.leftRight}px)`,
|
||||
background: theme.palette.ui03
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const DeepLinkingMobilePage: React.FC<WithTranslation> = ({ t }) => {
|
||||
const deeplinkingCfg = useSelector((state: IReduxState) =>
|
||||
state['features/base/config']?.deeplinking || {} as IDeeplinkingConfig);
|
||||
const { hideLogo } = deeplinkingCfg;
|
||||
const deepLinkingUrl: string = useSelector(generateDeepLinkingURL);
|
||||
const room = useSelector((state: IReduxState) => decodeURIComponent(state['features/base/conference'].room || ''));
|
||||
const url = useSelector((state: IReduxState) => state['features/base/connection'] || {});
|
||||
const dispatch = useDispatch();
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
const generateDownloadURL = useCallback(() => {
|
||||
const { downloadLink, dynamicLink, appScheme }
|
||||
= (deeplinkingCfg?.[Platform.OS as keyof typeof deeplinkingCfg] || {}) as IDeeplinkingMobileConfig;
|
||||
|
||||
if (downloadLink && typeof dynamicLink === 'undefined') {
|
||||
return downloadLink;
|
||||
}
|
||||
|
||||
const {
|
||||
apn,
|
||||
appCode,
|
||||
customDomain,
|
||||
ibi,
|
||||
isi
|
||||
} = dynamicLink || {};
|
||||
|
||||
const domain = customDomain ?? `https://${appCode}.app.goo.gl`;
|
||||
|
||||
return `${domain}/?link=${
|
||||
encodeURIComponent(window.location.href)}&apn=${
|
||||
apn}&ibi=${
|
||||
ibi}&isi=${
|
||||
isi}&ius=${
|
||||
appScheme}&efr=1`;
|
||||
}, [ deeplinkingCfg ]);
|
||||
|
||||
const onDownloadApp = useCallback(() => {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'downloadAppButton', { isMobileBrowser: true }));
|
||||
}, []);
|
||||
|
||||
const onLaunchWeb = useCallback(() => {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'launchWebButton', { isMobileBrowser: true }));
|
||||
dispatch(openWebApp());
|
||||
}, []);
|
||||
|
||||
const onOpenApp = useCallback(() => {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'openAppButton', { isMobileBrowser: true }));
|
||||
}, []);
|
||||
|
||||
const onOpenLinkProperties = useMemo(() => {
|
||||
const { downloadLink }
|
||||
= (deeplinkingCfg?.[Platform.OS as keyof typeof deeplinkingCfg] || {}) as IDeeplinkingMobileConfig;
|
||||
|
||||
if (downloadLink) {
|
||||
return {
|
||||
// When opening a link to the download page, we want to let the
|
||||
// OS itself handle intercepting and opening the appropriate
|
||||
// app store. This avoids potential issues with browsers, such
|
||||
// as iOS Chrome, not opening the store properly.
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
// When falling back to another URL (Firebase) let the page be
|
||||
// opened in a new window. This helps prevent the user getting
|
||||
// trapped in an app-open-cycle where going back to the mobile
|
||||
// browser re-triggers the app-open behavior.
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
};
|
||||
}, [ deeplinkingCfg ]);
|
||||
|
||||
useEffect(() => {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'displayed', 'DeepLinkingMobile', { isMobileBrowser: true }));
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className = { styles.container }>
|
||||
<div className = { styles.contentPane }>
|
||||
{!hideLogo && (<img
|
||||
alt = { t('welcomepage.logo.logoDeepLinking') }
|
||||
src = 'images/logo-deep-linking-mobile.png' />
|
||||
)}
|
||||
|
||||
<div className = { styles.launchingMeetingLabel }>{ t(`${_TNS}.launchMeetingLabel`) }</div>
|
||||
<div className = ''>{room}</div>
|
||||
<a
|
||||
{ ...onOpenLinkProperties }
|
||||
className = { styles.joinMeetWrapper }
|
||||
href = { deepLinkingUrl }
|
||||
onClick = { onOpenApp }
|
||||
target = '_top'>
|
||||
<Button
|
||||
fullWidth = { true }
|
||||
label = { t(`${_TNS}.joinInAppNew`) } />
|
||||
</a>
|
||||
<div className = { styles.labelDescription }>{ t(`${_TNS}.noMobileApp`) }</div>
|
||||
<a
|
||||
{ ...onOpenLinkProperties }
|
||||
className = { styles.linkWrapper }
|
||||
href = { generateDownloadURL() }
|
||||
onClick = { onDownloadApp }
|
||||
target = '_top'>
|
||||
<div className = { styles.linkLabel }>{ t(`${_TNS}.downloadMobileApp`) }</div>
|
||||
</a>
|
||||
{isSupportedMobileBrowser() ? (
|
||||
<div className = { styles.supportedBrowserContent }>
|
||||
<div className = { styles.labelOr }>OR</div>
|
||||
<a
|
||||
className = { styles.linkWrapper }
|
||||
onClick = { onLaunchWeb }
|
||||
target = '_top'>
|
||||
<div className = { styles.linkLabel }>{ t(`${_TNS}.joinInBrowser`) }</div>
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className = { styles.labelDescription }>
|
||||
{t(`${_TNS}.unsupportedBrowser`)}
|
||||
</div>
|
||||
)}
|
||||
<div className = { styles.separator } />
|
||||
<DialInSummary
|
||||
className = 'deep-linking-dial-in'
|
||||
clickableNumbers = { true }
|
||||
room = { room }
|
||||
url = { url } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(DeepLinkingMobilePage);
|
||||
@@ -1,9 +0,0 @@
|
||||
// @flow
|
||||
/**
|
||||
* Method used in order to render a custom promotional footer.
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function renderPromotionalFooter() {
|
||||
return null;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import Tabs from '@atlaskit/tabs';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
@@ -7,6 +6,7 @@ import { hideDialog } from '../../base/dialog/actions';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { connect } from '../../base/redux/functions';
|
||||
import Dialog from '../../base/ui/components/web/Dialog';
|
||||
import Tabs from '../../base/ui/components/web/Tabs';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { obtainDesktopSources } from '../functions';
|
||||
@@ -85,7 +85,7 @@ interface IState {
|
||||
/**
|
||||
* The desktop source type currently being displayed.
|
||||
*/
|
||||
selectedTab: number;
|
||||
selectedTab: string;
|
||||
|
||||
/**
|
||||
* An object containing all the DesktopCapturerSources.
|
||||
@@ -133,7 +133,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||
state: IState = {
|
||||
screenShareAudio: false,
|
||||
selectedSource: {},
|
||||
selectedTab: 0,
|
||||
selectedTab: DEFAULT_TAB_TYPE,
|
||||
sources: {},
|
||||
types: []
|
||||
};
|
||||
@@ -191,6 +191,8 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { selectedTab, selectedSource, sources } = this.state;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{
|
||||
@@ -202,6 +204,14 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||
size = 'large'
|
||||
titleKey = 'dialog.shareYourScreen'>
|
||||
{ this._renderTabs() }
|
||||
<DesktopPickerPane
|
||||
key = { selectedTab }
|
||||
onClick = { this._onPreviewClick }
|
||||
onDoubleClick = { this._onSubmit }
|
||||
onShareAudioChecked = { this._onShareAudioChecked }
|
||||
selectedSourceId = { selectedSource.id }
|
||||
sources = { sources[selectedTab as keyof typeof sources] }
|
||||
type = { selectedTab } />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -296,23 +306,20 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||
* Stores the selected tab and updates the selected source via
|
||||
* {@code _getSelectedSource}.
|
||||
*
|
||||
* @param {Object} _tab - The configuration passed into atlaskit tabs to
|
||||
* describe how to display the selected tab.
|
||||
* @param {number} tabIndex - The index of the tab within the array of
|
||||
* displayed tabs.
|
||||
* @param {string} id - The id of the newly selected tab.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTabSelected(_tab: Object, tabIndex: number) {
|
||||
const { types, sources } = this.state;
|
||||
_onTabSelected(id: string) {
|
||||
const { sources } = this.state;
|
||||
|
||||
this._selectedTabType = types[tabIndex];
|
||||
this._selectedTabType = id;
|
||||
|
||||
// When we change tabs also reset the screenShareAudio state so we don't
|
||||
// use the option from one tab when sharing from another.
|
||||
this.setState({
|
||||
screenShareAudio: false,
|
||||
selectedSource: this._getSelectedSource(sources),
|
||||
selectedTab: tabIndex
|
||||
selectedTab: id
|
||||
});
|
||||
}
|
||||
|
||||
@@ -334,27 +341,23 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderTabs() {
|
||||
const { selectedSource, sources, types } = this.state;
|
||||
const { types } = this.state;
|
||||
const { t } = this.props;
|
||||
const tabs
|
||||
= types.map(
|
||||
type => {
|
||||
return {
|
||||
content: <DesktopPickerPane
|
||||
key = { type }
|
||||
onClick = { this._onPreviewClick }
|
||||
onDoubleClick = { this._onSubmit }
|
||||
onShareAudioChecked = { this._onShareAudioChecked }
|
||||
selectedSourceId = { selectedSource.id }
|
||||
sources = { sources[type as keyof typeof sources] }
|
||||
type = { type } />,
|
||||
accessibilityLabel: t(TAB_LABELS[type as keyof typeof TAB_LABELS]),
|
||||
id: type,
|
||||
label: t(TAB_LABELS[type as keyof typeof TAB_LABELS])
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
onSelect = { this._onTabSelected }
|
||||
accessibilityLabel = ''
|
||||
className = 'desktop-picker-tabs-container'
|
||||
onChange = { this._onTabSelected }
|
||||
selected = { this.state.selectedTab }
|
||||
tabs = { tabs } />);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getAvailableDevices } from '../../base/devices/actions.web';
|
||||
import AbstractDialogTab, {
|
||||
type Props as AbstractDialogTabProps
|
||||
} from '../../base/dialog/components/web/AbstractDialogTab';
|
||||
@@ -81,12 +82,6 @@ export type Props = {
|
||||
*/
|
||||
hideVideoInputPreview: boolean,
|
||||
|
||||
/**
|
||||
* An optional callback to invoke after the component has completed its
|
||||
* mount logic.
|
||||
*/
|
||||
mountCallback?: Function,
|
||||
|
||||
/**
|
||||
* The id of the audio input device to preview.
|
||||
*/
|
||||
@@ -176,7 +171,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId)
|
||||
])
|
||||
.catch(err => logger.warn('Failed to initialize preview tracks', err))
|
||||
.then(() => this.props.mountCallback && this.props.mountCallback());
|
||||
.then(() => getAvailableDevices());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -760,7 +760,7 @@ export function isStageFilmstripTopPanel(state: IReduxState, minParticipantCount
|
||||
export function isStageFilmstripEnabled(state: IReduxState) {
|
||||
const { filmstrip } = state['features/base/config'];
|
||||
|
||||
return !filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP;
|
||||
return Boolean(!filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { addStageParticipant, setFilmstripVisible } from '../filmstrip/actions';
|
||||
import { addStageParticipant, removeStageParticipant, setFilmstripVisible } from '../filmstrip/actions';
|
||||
import { setTileView } from '../video-layout/actions.any';
|
||||
|
||||
import {
|
||||
@@ -180,9 +180,19 @@ function _onFollowMeCommand(attributes: any = {}, id: string, store: IStore) {
|
||||
|
||||
if (attributes.pinnedStageParticipants !== undefined) {
|
||||
const stageParticipants = JSON.parse(attributes.pinnedStageParticipants);
|
||||
let oldStageParticipants = [];
|
||||
|
||||
if (!_.isEqual(stageParticipants, oldState.pinnedStageParticipants)) {
|
||||
stageParticipants.forEach((p: { participantId: string; }) =>
|
||||
if (oldState.pinnedStageParticipants !== undefined) {
|
||||
oldStageParticipants = JSON.parse(oldState.pinnedStageParticipants);
|
||||
}
|
||||
|
||||
if (!_.isEqual(stageParticipants, oldStageParticipants)) {
|
||||
const toRemove = _.differenceWith(oldStageParticipants, stageParticipants, _.isEqual);
|
||||
const toAdd = _.differenceWith(stageParticipants, oldStageParticipants, _.isEqual);
|
||||
|
||||
toRemove.forEach((p: { participantId: string; }) =>
|
||||
store.dispatch(removeStageParticipant(p.participantId)));
|
||||
toAdd.forEach((p: { participantId: string; }) =>
|
||||
store.dispatch(addStageParticipant(p.participantId, true)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { _formatConferenceIDPin } from '../../../_utils';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ConferenceID}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The conference ID for dialing in.
|
||||
*/
|
||||
conferenceID: number,
|
||||
|
||||
/**
|
||||
* The name of the conference.
|
||||
*/
|
||||
conferenceName: ?string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a conference ID used as a pin for dialing into a conference.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ConferenceID extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { conferenceID, conferenceName, t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'dial-in-conference-id'>
|
||||
<div className = 'dial-in-conference-name'>
|
||||
{ conferenceName }
|
||||
</div>
|
||||
<div className = 'dial-in-conference-description'>
|
||||
{ t('info.dialANumber') }
|
||||
</div>
|
||||
<div className = 'dial-in-conference-pin'>
|
||||
{ `${t('info.dialInConferenceID')} ${_formatConferenceIDPin(conferenceID)}` }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(ConferenceID);
|
||||
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { withPixelLineHeight } from '../../../../base/styles/functions.web';
|
||||
// @ts-ignore
|
||||
import { _formatConferenceIDPin } from '../../../_utils';
|
||||
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The conference id.
|
||||
*/
|
||||
conferenceID?: string | number;
|
||||
|
||||
/**
|
||||
* The conference name.
|
||||
*/
|
||||
conferenceName: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginTop: 32,
|
||||
maxWidth: 310,
|
||||
padding: '16px 12px',
|
||||
background: theme.palette.ui02,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 6
|
||||
},
|
||||
confNameLabel: {
|
||||
...withPixelLineHeight(theme.typography.heading6),
|
||||
marginBottom: 18,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
},
|
||||
descriptionLabel: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
|
||||
marginBottom: 18
|
||||
},
|
||||
separator: {
|
||||
width: '100%',
|
||||
height: 1,
|
||||
background: theme.palette.ui04,
|
||||
marginBottom: 18
|
||||
},
|
||||
pinLabel: {
|
||||
...withPixelLineHeight(theme.typography.heading6)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const ConferenceID: React.FC<IProps> = ({ conferenceID, t }) => {
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { styles.container }>
|
||||
<div className = { styles.descriptionLabel }>
|
||||
To join the meeting via phone, dial one of these numbers and then enter the pin
|
||||
</div>
|
||||
<div className = { styles.separator } />
|
||||
<div className = { styles.pinLabel }>
|
||||
{ `${t('info.dialInConferenceID')} ${_formatConferenceIDPin(conferenceID ?? '')}` }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(ConferenceID);
|
||||
@@ -1,8 +1,13 @@
|
||||
// @flow
|
||||
|
||||
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import clsx from 'clsx';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { withPixelLineHeight } from '../../../../base/styles/functions.web';
|
||||
import { getDialInConferenceID, getDialInNumbers } from '../../../_utils';
|
||||
|
||||
import ConferenceID from './ConferenceID';
|
||||
@@ -20,6 +25,11 @@ type Props = {
|
||||
*/
|
||||
className: string,
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Whether or not numbers should include links with the telephone protocol.
|
||||
*/
|
||||
@@ -30,6 +40,16 @@ type Props = {
|
||||
*/
|
||||
room: string,
|
||||
|
||||
/**
|
||||
* Whether the dial in summary container is scrollable.
|
||||
*/
|
||||
scrollable: Boolean,
|
||||
|
||||
/**
|
||||
* Whether the room name should show as title.
|
||||
*/
|
||||
showTitle?: boolean,
|
||||
|
||||
/**
|
||||
* The url where we were loaded.
|
||||
*/
|
||||
@@ -72,6 +92,26 @@ type State = {
|
||||
numbersEnabled: ?boolean
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
hasNumbers: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#1E1E1E',
|
||||
color: theme.palette.text01
|
||||
},
|
||||
scrollable: {
|
||||
height: '100vh',
|
||||
overflowY: 'scroll'
|
||||
},
|
||||
roomName: {
|
||||
margin: '40px auto 8px',
|
||||
...withPixelLineHeight(theme.typography.heading5)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a page listing numbers for dialing into a conference and pin to
|
||||
* the a specific conference.
|
||||
@@ -136,24 +176,27 @@ class DialInSummary extends Component<Props, State> {
|
||||
let contents;
|
||||
|
||||
const { conferenceID, error, loading, numbersEnabled } = this.state;
|
||||
const { classes, showTitle, room, clickableNumbers, scrollable, t } = this.props;
|
||||
|
||||
if (loading) {
|
||||
contents = '';
|
||||
} else if (numbersEnabled === false) {
|
||||
contents = this.props.t('info.dialInNotSupported');
|
||||
contents = t('info.dialInNotSupported');
|
||||
} else if (error) {
|
||||
contents = error;
|
||||
} else {
|
||||
className = 'has-numbers';
|
||||
className = clsx(classes.hasNumbers, scrollable && classes.scrollable);
|
||||
contents = [
|
||||
conferenceID
|
||||
? <ConferenceID
|
||||
conferenceID = { conferenceID }
|
||||
conferenceName = { this.props.room }
|
||||
key = 'conferenceID' />
|
||||
: null,
|
||||
? <>
|
||||
{ showTitle && <div className = { classes.roomName }>{ room }</div> }
|
||||
<ConferenceID
|
||||
conferenceID = { conferenceID }
|
||||
conferenceName = { room }
|
||||
key = 'conferenceID' />
|
||||
</> : null,
|
||||
<NumbersList
|
||||
clickableNumbers = { this.props.clickableNumbers }
|
||||
clickableNumbers = { clickableNumbers }
|
||||
conferenceID = { conferenceID }
|
||||
key = 'numbers'
|
||||
numbers = { this.state.numbers } />
|
||||
@@ -161,7 +204,7 @@ class DialInSummary extends Component<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { `${this.props.className} ${className}` }>
|
||||
<div className = { className }>
|
||||
{ contents }
|
||||
</div>
|
||||
);
|
||||
@@ -272,4 +315,4 @@ class DialInSummary extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(DialInSummary);
|
||||
export default translate(withStyles(styles)(DialInSummary));
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// @flow
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React from 'react';
|
||||
|
||||
import { BaseApp } from '../../../../base/app';
|
||||
import { isMobileBrowser } from '../../../../base/environment/utils';
|
||||
import GlobalStyles from '../../../../base/ui/components/GlobalStyles.web';
|
||||
import JitsiThemeProvider from '../../../../base/ui/components/JitsiThemeProvider.web';
|
||||
import { parseURLParams } from '../../../../base/util';
|
||||
import { DIAL_IN_INFO_PAGE_PATH_NAME } from '../../../constants';
|
||||
import NoRoomError from '../../dial-in-info-page/NoRoomError.web';
|
||||
|
||||
import DialInSummary from './DialInSummary';
|
||||
|
||||
/**
|
||||
* Wrapper application for prejoin.
|
||||
*
|
||||
* @augments BaseApp
|
||||
*/
|
||||
export default class DialInSummaryApp extends BaseApp {
|
||||
/**
|
||||
* The deferred for the initialisation {{promise, resolve, reject}}.
|
||||
*/
|
||||
_init: Object;
|
||||
|
||||
/**
|
||||
* Navigates to {@link Prejoin} upon mount.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async componentDidMount() {
|
||||
await super.componentDidMount();
|
||||
|
||||
const { room } = parseURLParams(window.location, true, 'search');
|
||||
const { href } = window.location;
|
||||
const ix = href.indexOf(DIAL_IN_INFO_PAGE_PATH_NAME);
|
||||
const url = (ix > 0 ? href.substring(0, ix) : href) + room;
|
||||
|
||||
super._navigate({
|
||||
component: () => (<>
|
||||
{room
|
||||
? <DialInSummary
|
||||
className = 'dial-in-page'
|
||||
clickableNumbers = { isMobileBrowser() }
|
||||
room = { decodeURIComponent(room) }
|
||||
scrollable = { true }
|
||||
showTitle = { true }
|
||||
url = { url } />
|
||||
: <NoRoomError className = 'dial-in-page' />}
|
||||
</>)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
|
||||
* the top most component.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_createMainElement(component, props) {
|
||||
return (
|
||||
<JitsiThemeProvider>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
<GlobalStyles />
|
||||
{super._createMainElement(component, props)}
|
||||
</AtlasKitThemeProvider>
|
||||
</JitsiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderDialogContainer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { Icon, IconPhoneRinging } from '../../../../base/icons';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether or not numbers should include links with the telephone protocol.
|
||||
*/
|
||||
clickableNumbers: boolean,
|
||||
|
||||
/**
|
||||
* The conference ID for dialing in.
|
||||
*/
|
||||
conferenceID: number,
|
||||
|
||||
/**
|
||||
* The phone numbers to display. Can be an array of number Objects or an
|
||||
* object with countries as keys and an array of numbers as values.
|
||||
*/
|
||||
numbers: { [string]: Array<string> } | Array<Object>,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a table with phone numbers to dial in to a conference.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class NumbersList extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { numbers } = this.props;
|
||||
|
||||
return this._renderWithCountries(numbers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders rows of countries and associated phone numbers.
|
||||
*
|
||||
* @param {Object|Array<Object>} numbersMapping - An object with country
|
||||
* names as keys and values as arrays of phone numbers.
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
*/
|
||||
_renderWithCountries(
|
||||
numbersMapping: { numbers: Array<string> } | Array<Object>) {
|
||||
const { t } = this.props;
|
||||
let hasFlags = false, numbers;
|
||||
|
||||
if (Array.isArray(numbersMapping)) {
|
||||
hasFlags = true;
|
||||
numbers = numbersMapping.reduce(
|
||||
(resultNumbers, number) => {
|
||||
// The i18n-iso-countries package insists on upper case.
|
||||
const countryCode = number.countryCode.toUpperCase();
|
||||
|
||||
let countryName;
|
||||
|
||||
if (countryCode === 'SIP') {
|
||||
countryName = t('info.sip');
|
||||
} else {
|
||||
countryName = t(`countries:countries.${countryCode}`);
|
||||
|
||||
// Some countries have multiple names as US ['United States of America', 'USA']
|
||||
// choose the first one if that is the case
|
||||
if (!countryName) {
|
||||
countryName = t(`countries:countries.${countryCode}.0`);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultNumbers[countryName]) {
|
||||
resultNumbers[countryName].push(number);
|
||||
} else {
|
||||
resultNumbers[countryName] = [ number ];
|
||||
}
|
||||
|
||||
return resultNumbers;
|
||||
}, {});
|
||||
} else {
|
||||
numbers = {};
|
||||
|
||||
for (const [ country, numbersArray ]
|
||||
of Object.entries(numbersMapping.numbers)) {
|
||||
|
||||
if (Array.isArray(numbersArray)) {
|
||||
/* eslint-disable arrow-body-style */
|
||||
const formattedNumbers = numbersArray.map(number => ({
|
||||
formattedNumber: number
|
||||
}));
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
numbers[country] = formattedNumbers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
|
||||
Object.keys(numbers).forEach((countryName: string) => {
|
||||
const numbersArray = numbers[countryName];
|
||||
|
||||
rows.push(
|
||||
<tr
|
||||
className = 'number-group'
|
||||
key = { countryName }>
|
||||
{ this._renderFlag(numbersArray[0].countryCode) }
|
||||
<td className = 'country' >{ countryName }</td>
|
||||
<td className = 'numbers-list-column'>
|
||||
{ this._renderNumbersList(numbersArray) }
|
||||
</td>
|
||||
<td className = 'toll-free-list-column' >
|
||||
{ this._renderNumbersTollFreeList(numbersArray) }
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<table className = 'dial-in-numbers-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
{ hasFlags ? <th /> : null}
|
||||
<th>{ t('info.country') }</th>
|
||||
<th>{ t('info.numbers') }</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className = 'dial-in-numbers-body'>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a div container for a flag for the country of the phone number.
|
||||
*
|
||||
* @param {string} countryCode - The country code flag to display.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFlag(countryCode) {
|
||||
if (countryCode) {
|
||||
return (
|
||||
<td className = 'flag-cell'>
|
||||
{countryCode === 'SIP'
|
||||
? <Icon src = { IconPhoneRinging } />
|
||||
: <i className = { `flag iti-flag ${countryCode}` } />
|
||||
}
|
||||
</td>);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a div container for a phone number.
|
||||
*
|
||||
* @param {Array} numbers - The phone number to display.
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
*/
|
||||
_renderNumbersList(numbers) {
|
||||
const numbersListItems = numbers.map(number =>
|
||||
(<li
|
||||
className = 'dial-in-number'
|
||||
key = { number.formattedNumber }>
|
||||
{ this._renderNumberLink(number.formattedNumber) }
|
||||
</li>));
|
||||
|
||||
return (
|
||||
<ul className = 'numbers-list'>
|
||||
{ numbersListItems }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders list with a toll free text on the position where there is a
|
||||
* number marked as toll free.
|
||||
*
|
||||
* @param {Array} numbers - The phone number that are displayed.
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
*/
|
||||
_renderNumbersTollFreeList(numbers) {
|
||||
const { t } = this.props;
|
||||
|
||||
const tollNumbersListItems = numbers.map(number =>
|
||||
(<li
|
||||
className = 'toll-free'
|
||||
key = { number.formattedNumber }>
|
||||
{ number.tollFree ? t('info.dialInTollFree') : '' }
|
||||
</li>));
|
||||
|
||||
return (
|
||||
<ul className = 'toll-free-list'>
|
||||
{ tollNumbersListItems }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a ReactElement for displaying a telephone number. If the
|
||||
* component prop {@code clickableNumbers} is true, then the number will
|
||||
* have a link with the telephone protocol.
|
||||
*
|
||||
* @param {string} number - The phone number to display.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderNumberLink(number) {
|
||||
if (this.props.clickableNumbers) {
|
||||
// Url encode # to %23, Android phone was cutting the # after
|
||||
// clicking it.
|
||||
// Seems that using ',' and '%23' works on iOS and Android.
|
||||
return (
|
||||
<a
|
||||
href = { `tel:${number},${this.props.conferenceID}%23` }
|
||||
key = { number } >
|
||||
{ number }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(NumbersList);
|
||||
@@ -0,0 +1,209 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
import countries from 'i18n-iso-countries';
|
||||
import en from 'i18n-iso-countries/langs/en.json';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
// @ts-ignore
|
||||
import { Icon, IconSip } from '../../../../base/icons';
|
||||
|
||||
countries.registerLocale(en);
|
||||
|
||||
interface INormalizedNumber {
|
||||
|
||||
/**
|
||||
* The country code.
|
||||
*/
|
||||
countryCode?: string;
|
||||
|
||||
/**
|
||||
* The formatted number.
|
||||
*/
|
||||
formattedNumber: string;
|
||||
|
||||
/**
|
||||
* Whether the number is toll-free.
|
||||
*/
|
||||
tollFree?: boolean;
|
||||
}
|
||||
|
||||
interface INumbersMapping {
|
||||
[countryName: string]: Array<INormalizedNumber>;
|
||||
}
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether or not numbers should include links with the telephone protocol.
|
||||
*/
|
||||
clickableNumbers: boolean;
|
||||
|
||||
/**
|
||||
* The conference ID for dialing in.
|
||||
*/
|
||||
conferenceID: number;
|
||||
|
||||
/**
|
||||
* The phone numbers to display. Can be an array of number Objects or an
|
||||
* object with countries as keys and an array of numbers as values.
|
||||
*/
|
||||
numbers: INumbersMapping;
|
||||
|
||||
}
|
||||
|
||||
const NumbersList: React.FC<IProps> = ({ t, conferenceID, clickableNumbers, numbers: numbersMapping }) => {
|
||||
const renderFlag = useCallback((countryCode: string) => {
|
||||
if (countryCode) {
|
||||
return (
|
||||
<td className = 'flag-cell'>
|
||||
{countryCode === 'SIP'
|
||||
? <Icon src = { IconSip } />
|
||||
: <i className = { `flag iti-flag ${countryCode}` } />
|
||||
}
|
||||
</td>);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const renderNumberLink = useCallback((number: string) => {
|
||||
if (clickableNumbers) {
|
||||
// Url encode # to %23, Android phone was cutting the # after
|
||||
// clicking it.
|
||||
// Seems that using ',' and '%23' works on iOS and Android.
|
||||
return (
|
||||
<a
|
||||
href = { `tel:${number},${conferenceID}%23` }
|
||||
key = { number } >
|
||||
{number}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return number;
|
||||
}, [ conferenceID, clickableNumbers ]);
|
||||
|
||||
const renderNumbersList = useCallback((numbers: Array<INormalizedNumber>) => {
|
||||
const numbersListItems = numbers.map(number =>
|
||||
(<li
|
||||
className = 'dial-in-number'
|
||||
key = { number.formattedNumber }>
|
||||
{renderNumberLink(number.formattedNumber)}
|
||||
</li>));
|
||||
|
||||
return (
|
||||
<ul className = 'numbers-list'>
|
||||
{numbersListItems}
|
||||
</ul>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderNumbersTollFreeList = useCallback((numbers: Array<INormalizedNumber>) => {
|
||||
const tollNumbersListItems = numbers.map(number =>
|
||||
(<li
|
||||
className = 'toll-free'
|
||||
key = { number.formattedNumber }>
|
||||
{number.tollFree ? t('info.dialInTollFree') : ''}
|
||||
</li>));
|
||||
|
||||
return (
|
||||
<ul className = 'toll-free-list'>
|
||||
{tollNumbersListItems}
|
||||
</ul>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderNumbers = useMemo(() => {
|
||||
let numbers: INumbersMapping;
|
||||
|
||||
if (!numbersMapping) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(numbersMapping)) {
|
||||
numbers = numbersMapping.reduce(
|
||||
(resultNumbers: any, number: any) => {
|
||||
// The i18n-iso-countries package insists on upper case.
|
||||
const countryCode = number.countryCode.toUpperCase();
|
||||
let countryName;
|
||||
|
||||
if (countryCode === 'SIP') {
|
||||
countryName = t('info.sip');
|
||||
} else {
|
||||
countryName = t(`countries:countries.${countryCode}`);
|
||||
|
||||
// Some countries have multiple names as US ['United States of America', 'USA']
|
||||
// choose the first one if that is the case
|
||||
if (!countryName) {
|
||||
countryName = t(`countries:countries.${countryCode}.0`);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultNumbers[countryName]) {
|
||||
resultNumbers[countryName].push(number);
|
||||
} else {
|
||||
resultNumbers[countryName] = [ number ];
|
||||
}
|
||||
|
||||
return resultNumbers;
|
||||
}, {});
|
||||
} else {
|
||||
numbers = {};
|
||||
|
||||
for (const [ country, numbersArray ]
|
||||
of Object.entries(numbersMapping.numbers)) {
|
||||
|
||||
if (Array.isArray(numbersArray)) {
|
||||
/* eslint-disable arrow-body-style */
|
||||
const formattedNumbers = numbersArray.map(number => ({
|
||||
formattedNumber: number
|
||||
}));
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
numbers[country] = formattedNumbers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows: [JSX.Element] = [] as unknown as [JSX.Element];
|
||||
|
||||
Object.keys(numbers).forEach((countryName: string) => {
|
||||
const numbersArray: Array<INormalizedNumber> = numbers[countryName];
|
||||
const countryCode = numbersArray[0].countryCode
|
||||
|| countries.getAlpha2Code(countryName, 'en')?.toUpperCase()
|
||||
|| countryName;
|
||||
|
||||
rows.push(
|
||||
<>
|
||||
<tr
|
||||
key = { countryName }>
|
||||
{renderFlag(countryCode)}
|
||||
<td className = 'country' >{countryName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td />
|
||||
<td className = 'numbers-list-column'>
|
||||
{renderNumbersList(numbersArray)}
|
||||
</td>
|
||||
<td className = 'toll-free-list-column' >
|
||||
{renderNumbersTollFreeList(numbersArray)}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
return rows;
|
||||
}, [ numbersMapping ]);
|
||||
|
||||
return (
|
||||
<table className = 'dial-in-numbers-list'>
|
||||
<tbody className = 'dial-in-numbers-body'>
|
||||
{renderNumbers}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(NumbersList);
|
||||
@@ -1,173 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { isLocalParticipantModerator } from '../../../base/participants/functions';
|
||||
import { connect } from '../../../base/redux';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { handleLobbyChatInitialized } from '../../../chat/actions.native';
|
||||
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import ParticipantItem
|
||||
from '../../../participants-pane/components/native/ParticipantItem';
|
||||
import { setKnockingParticipantApproval } from '../../actions.native';
|
||||
import { getKnockingParticipants, getLobbyEnabled, showLobbyChatButton } from '../../functions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* Props type of the component.
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The list of participants.
|
||||
*/
|
||||
_participants: Array<Object>,
|
||||
|
||||
/**
|
||||
* True if the list should be rendered.
|
||||
*/
|
||||
_visible: boolean,
|
||||
|
||||
/**
|
||||
* True if the polls feature is disabled.
|
||||
*/
|
||||
_isPollsDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Returns true if the lobby chat button should be shown.
|
||||
*/
|
||||
_showChatButton: Function,
|
||||
|
||||
/**
|
||||
* The Redux Dispatch function.
|
||||
*/
|
||||
dispatch: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to render a list for the actively knocking participants.
|
||||
*/
|
||||
class KnockingParticipantList extends PureComponent<Props> {
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onRespondToParticipant = this._onRespondToParticipant.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _participants, _visible, _showChatButton } = this.props;
|
||||
|
||||
if (!_visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ _participants.map(p => (
|
||||
<View
|
||||
key = { p.id }
|
||||
style = { styles.knockingParticipantListEntry }>
|
||||
<ParticipantItem
|
||||
displayName = { p.name }
|
||||
isKnockingParticipant = { true }
|
||||
key = { p.id }
|
||||
participantID = { p.id }>
|
||||
<Button
|
||||
labelKey = { 'lobby.admit' }
|
||||
onClick = { this._onRespondToParticipant(p.id, true) }
|
||||
style = { styles.lobbyButtonAdmit }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
{
|
||||
_showChatButton(p)
|
||||
? (
|
||||
<Button
|
||||
labelKey = { 'lobby.chat' }
|
||||
onClick = { this._onInitializeLobbyChat(p.id) }
|
||||
style = { styles.lobbyButtonChat }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
) : null
|
||||
}
|
||||
<Button
|
||||
labelKey = { 'lobby.reject' }
|
||||
onClick = { this._onRespondToParticipant(p.id, false) }
|
||||
style = { styles.lobbyButtonReject }
|
||||
type = { BUTTON_TYPES.DESTRUCTIVE } />
|
||||
</ParticipantItem>
|
||||
</View>
|
||||
)) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
_onRespondToParticipant: (string, boolean) => Function;
|
||||
|
||||
/**
|
||||
* Function that constructs a callback for the response handler button.
|
||||
*
|
||||
* @param {string} id - The id of the knocking participant.
|
||||
* @param {boolean} approve - The response for the knocking.
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onRespondToParticipant(id, approve) {
|
||||
return () => {
|
||||
this.props.dispatch(setKnockingParticipantApproval(id, approve));
|
||||
};
|
||||
}
|
||||
|
||||
_onInitializeLobbyChat: (string) => Function;
|
||||
|
||||
/**
|
||||
* Function that constructs a callback for the lobby chat button.
|
||||
*
|
||||
* @param {string} id - The id of the knocking participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onInitializeLobbyChat(id) {
|
||||
return () => {
|
||||
this.props.dispatch(handleLobbyChatInitialized(id));
|
||||
if (this.props._isPollsDisabled) {
|
||||
return navigate(screen.conference.chat);
|
||||
}
|
||||
|
||||
navigate(screen.conference.chatandpolls.main);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state): Object {
|
||||
const lobbyEnabled = getLobbyEnabled(state);
|
||||
const knockingParticipants = getKnockingParticipants(state);
|
||||
const { disablePolls } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_visible: lobbyEnabled && isLocalParticipantModerator(state),
|
||||
_showChatButton: participant => showLobbyChatButton(participant)(state),
|
||||
_isPollsDisabled: disablePolls,
|
||||
|
||||
// On mobile we only show a portion of the list for screen real estate reasons
|
||||
_participants: knockingParticipants.slice(0, 2)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(KnockingParticipantList));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user