mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-09-14 03:28:41 +00:00
Compare commits
100 Commits
android-sd
...
pagination
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a21a515c40 | ||
|
|
09af88088d | ||
|
|
2e539ba010 | ||
|
|
87b3ec2cc0 | ||
|
|
907b51925d | ||
|
|
643340c4a6 | ||
|
|
d6c821d524 | ||
|
|
eb16f93153 | ||
|
|
47576aebba | ||
|
|
bac0a55421 | ||
|
|
1c8103c444 | ||
|
|
4e83e93eb6 | ||
|
|
0f8fa4f059 | ||
|
|
becaf0806a | ||
|
|
5b77d722d7 | ||
|
|
f4cde2192e | ||
|
|
e91df47d1b | ||
|
|
2d04f3852c | ||
|
|
2209394d09 | ||
|
|
1e76dc0aa2 | ||
|
|
75edfc1fab | ||
|
|
8c20dd8e47 | ||
|
|
fefe451180 | ||
|
|
b268e01a42 | ||
|
|
d62e378528 | ||
|
|
e8ad2365b6 | ||
|
|
b7389e1c31 | ||
|
|
eeddf6b350 | ||
|
|
665b7730ee | ||
|
|
7854437e31 | ||
|
|
600af62945 | ||
|
|
88ddb8d9b4 | ||
|
|
5182a720f9 | ||
|
|
415562c315 | ||
|
|
53d0a892b5 | ||
|
|
9b220f3870 | ||
|
|
c6e50ad439 | ||
|
|
36cb896680 | ||
|
|
249515ac60 | ||
|
|
80b49266ab | ||
|
|
1afae50923 | ||
|
|
b332fb474b | ||
|
|
a12ad99ecf | ||
|
|
400f47963d | ||
|
|
65fbc6f256 | ||
|
|
e7a324185f | ||
|
|
14a5c45fa3 | ||
|
|
05e6dde341 | ||
|
|
e13473d42f | ||
|
|
4b72fefd7e | ||
|
|
ba9398a1e2 | ||
|
|
8d4cf7165e | ||
|
|
0b3991d9e1 | ||
|
|
47be509d17 | ||
|
|
ba64d3e0c8 | ||
|
|
cd05c34d19 | ||
|
|
24550777c6 | ||
|
|
ee101f8947 | ||
|
|
8ca85f9e1c | ||
|
|
34ccd56691 | ||
|
|
f49c05c666 | ||
|
|
e7280e5040 | ||
|
|
eb1add681f | ||
|
|
8419dc725c | ||
|
|
f984faef3f | ||
|
|
0c76d7532c | ||
|
|
cb0b68f840 | ||
|
|
08a4da22f3 | ||
|
|
bdd6638067 | ||
|
|
8b44e06f2c | ||
|
|
79edc1b358 | ||
|
|
6597bfc2aa | ||
|
|
e0a2320d75 | ||
|
|
81e9fca03b | ||
|
|
76f8302aeb | ||
|
|
7263829763 | ||
|
|
b7ab3ea052 | ||
|
|
c657f360e1 | ||
|
|
ae5edf5a62 | ||
|
|
2bac757ca6 | ||
|
|
c10805f81b | ||
|
|
251eec19cd | ||
|
|
4276f82c03 | ||
|
|
4c3aae1e28 | ||
|
|
12be14bd4b | ||
|
|
420a7d8110 | ||
|
|
17f77a4246 | ||
|
|
6f9944a2d0 | ||
|
|
73328810e4 | ||
|
|
bb8c30a6c9 | ||
|
|
c5438ecd0c | ||
|
|
e22a25b216 | ||
|
|
4075e5deb7 | ||
|
|
ea0d953d1c | ||
|
|
b3e03fe50c | ||
|
|
8f81a75a61 | ||
|
|
0ab905bf75 | ||
|
|
5a3607f63f | ||
|
|
d57e202d19 | ||
|
|
1223c63f69 |
@@ -70,11 +70,12 @@ dependencies {
|
||||
implementation project(':react-native-default-preference')
|
||||
implementation project(':react-native-immersive')
|
||||
implementation project(':react-native-keep-awake')
|
||||
implementation project(':react-native-slider')
|
||||
implementation project(':react-native-sound')
|
||||
implementation project(':react-native-splash-screen')
|
||||
implementation project(':react-native-svg')
|
||||
implementation project(':react-native-webrtc')
|
||||
implementation project(':react-native-webview')
|
||||
implementation project(':react-native-splash-screen')
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
|
||||
<service
|
||||
android:name=".ConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -187,6 +187,7 @@ class ReactInstanceManagerHolder {
|
||||
new com.ocetnik.timer.BackgroundTimerPackage(),
|
||||
new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
|
||||
new com.reactnativecommunity.netinfo.NetInfoPackage(),
|
||||
new com.reactnativecommunity.slider.ReactSliderPackage(),
|
||||
new com.reactnativecommunity.webview.RNCWebViewPackage(),
|
||||
new com.rnimmersive.RNImmersivePackage(),
|
||||
new com.zmxv.RNSound.RNSoundPackage(),
|
||||
|
||||
@@ -19,6 +19,8 @@ include ':react-native-immersive'
|
||||
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
|
||||
include ':react-native-keep-awake'
|
||||
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
|
||||
include ':react-native-slider'
|
||||
project(':react-native-slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')
|
||||
include ':react-native-sound'
|
||||
project(':react-native-sound').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sound/android')
|
||||
include ':react-native-splash-screen'
|
||||
@@ -28,4 +30,4 @@ project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../
|
||||
include ':react-native-webrtc'
|
||||
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
|
||||
include ':react-native-webview'
|
||||
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
|
||||
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
|
||||
|
||||
46
config.js
46
config.js
@@ -70,6 +70,9 @@ var config = {
|
||||
// callStatsThreshold: 5 // enable callstats for 5% of the users.
|
||||
},
|
||||
|
||||
// Enables reactions feature.
|
||||
enableReactions: false,
|
||||
|
||||
// Disables ICE/UDP by filtering out local and remote UDP candidates in
|
||||
// signalling.
|
||||
// webrtcIceUdpDisable: false,
|
||||
@@ -459,11 +462,38 @@ var config = {
|
||||
// - 'desktop' controls the "Share your screen" button
|
||||
// - if `toolbarButtons` is undefined, we fallback to enabling all buttons on the UI
|
||||
// toolbarButtons: [
|
||||
// 'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
|
||||
// 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
// 'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
|
||||
// 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
// 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
|
||||
// 'camera',
|
||||
// 'chat',
|
||||
// 'closedcaptions',
|
||||
// 'desktop',
|
||||
// 'download',
|
||||
// 'embedmeeting',
|
||||
// 'etherpad',
|
||||
// 'feedback',
|
||||
// 'filmstrip',
|
||||
// 'fullscreen',
|
||||
// 'hangup',
|
||||
// 'help',
|
||||
// 'invite',
|
||||
// 'livestreaming',
|
||||
// 'microphone',
|
||||
// 'mute-everyone',
|
||||
// 'mute-video-everyone',
|
||||
// 'participants-pane',
|
||||
// 'profile',
|
||||
// 'raisehand',
|
||||
// 'recording',
|
||||
// 'security',
|
||||
// 'select-background',
|
||||
// 'settings',
|
||||
// 'shareaudio',
|
||||
// 'sharedvideo',
|
||||
// 'shortcuts',
|
||||
// 'stats',
|
||||
// 'tileview',
|
||||
// 'toggle-camera',
|
||||
// 'videoquality',
|
||||
// '__end'
|
||||
// ],
|
||||
|
||||
// Stats
|
||||
@@ -603,6 +633,9 @@ var config = {
|
||||
// conference (if set to true, these sounds will not be played).
|
||||
// disableJoinLeaveSounds: false,
|
||||
|
||||
// Disables the sounds that play when a chat message is received.
|
||||
// disableIncomingMessageSound: false,
|
||||
|
||||
// Information for the chrome extension banner
|
||||
// chromeExtensionBanner: {
|
||||
// // The chrome extension to be installed address
|
||||
@@ -732,6 +765,9 @@ var config = {
|
||||
// Hides the conference subject
|
||||
// hideConferenceSubject: true,
|
||||
|
||||
// Hides the recording label
|
||||
// hideRecordingLabel: false,
|
||||
|
||||
// Hides the conference timer.
|
||||
// hideConferenceTimer: true,
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ import {
|
||||
JitsiConnectionErrors,
|
||||
JitsiConnectionEvents
|
||||
} from './react/features/base/lib-jitsi-meet';
|
||||
import { isVpaasMeeting } from './react/features/billing-counter/functions';
|
||||
import { getJaasJWT } from './react/features/jaas/functions';
|
||||
import { isVpaasMeeting, getJaasJWT } from './react/features/jaas/functions';
|
||||
import { setPrejoinDisplayNameRequired } from './react/features/prejoin/actions';
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
@@ -89,8 +88,9 @@ export async function connect(id, password, roomName) {
|
||||
const connectionConfig = Object.assign({}, config);
|
||||
const state = APP.store.getState();
|
||||
let { jwt } = state['features/base/jwt'];
|
||||
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
|
||||
|
||||
if (!jwt && isVpaasMeeting(state, false)) {
|
||||
if (!iAmRecorder && !iAmSipGateway && !jwt && isVpaasMeeting(state)) {
|
||||
jwt = await getJaasJWT(state);
|
||||
APP.store.dispatch(setJWT(jwt));
|
||||
}
|
||||
|
||||
@@ -4,16 +4,28 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $drawerZ;
|
||||
background-color: #141414;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.drawer-portal::after {
|
||||
content: '';
|
||||
background-color: $participantsPaneBgColor;
|
||||
margin-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.drawer-menu-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.drawer-menu {
|
||||
max-height: calc(80vh - 64px);
|
||||
background: #242528;
|
||||
border-radius: 16px 16px 0 0;
|
||||
overflow-y: hidden;
|
||||
margin-bottom: env(safe-area-inset-bottom, 0);
|
||||
width: 100%;
|
||||
|
||||
.drawer-toggle {
|
||||
display: flex;
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
width: 20%;
|
||||
bottom: 0;
|
||||
left: 40%;
|
||||
height: 48px;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.reactions-menu-popup-container,
|
||||
@@ -111,8 +111,8 @@ $reactionCount: 20;
|
||||
line-height: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
top: 32px;
|
||||
left: 10px;
|
||||
top: 0;
|
||||
left: 20px;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
|
||||
@@ -123,8 +123,8 @@ $reactionCount: 20;
|
||||
@for $i from 1 through $reactionCount {
|
||||
&.reaction-#{$i} {
|
||||
animation: animation-#{$i} 5s forwards ease-in-out;
|
||||
top: #{random(50, 0)}px;
|
||||
left: #{random(-10, 10)}px;
|
||||
top: #{random(-40, 10)}px;
|
||||
left: #{random(0, 30)}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,31 @@
|
||||
z-index: $zindex3;
|
||||
|
||||
&.visible {
|
||||
top: 0px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.recording {
|
||||
top: 0;
|
||||
|
||||
.subject-details-container {
|
||||
opacity: 0;
|
||||
transition: opacity .3s ease-in;
|
||||
}
|
||||
|
||||
.subject-info-container .show-always {
|
||||
transition: margin-left .3s ease-in;
|
||||
}
|
||||
|
||||
&.visible {
|
||||
.subject-details-container {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subject-details-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.subject-info-container {
|
||||
|
||||
@@ -105,12 +105,15 @@
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
pointer-events: all;
|
||||
background-color: #131519;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toolbox-content-wrapper::after {
|
||||
content: '';
|
||||
background: $newToolbarBackgroundColor;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.toolbox-content-items {
|
||||
background: $newToolbarBackgroundColor;
|
||||
border-radius: 6px;
|
||||
@@ -118,6 +121,7 @@
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
pointer-events: all;
|
||||
box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
|
||||
|
||||
>div {
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
z-index: $subtitlesZ;
|
||||
|
||||
&.lifted {
|
||||
// Lift subtitle above toolbar+invite box.
|
||||
bottom: $newToolbarSize + 112px + 40px;
|
||||
// Lift subtitle above toolbar+dominant speaker box.
|
||||
bottom: $newToolbarSize + 36px + 40px;
|
||||
}
|
||||
|
||||
span {
|
||||
|
||||
@@ -46,6 +46,7 @@ $menuBG:#242528;
|
||||
$newToolbarFontSize: 24px;
|
||||
$newToolbarHangupFontSize: 32px;
|
||||
$newToolbarSize: 48px;
|
||||
$newToolbarSizeMobile: 60px;
|
||||
$newToolbarSizeWithPadding: calc(#{$newToolbarSize} + 24px);
|
||||
$toolbarTitleFontSize: 19px;
|
||||
$overflowMenuItemColor: #fff;
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: margin-bottom .3s ease-in;
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
@@ -52,11 +53,23 @@
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
|
||||
.remote-videos{
|
||||
.remote-videos {
|
||||
width: calc(100vw - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.collapse {
|
||||
#remoteVideos {
|
||||
height: calc(100% - #{$newToolbarSizeMobile}) !important;
|
||||
margin-bottom: $newToolbarSizeMobile;
|
||||
}
|
||||
|
||||
.remote-videos {
|
||||
// !important is needed here as overflow is set via element.style in a FixedSizeGrid.
|
||||
overflow: hidden auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
.virtual-background-none:hover {
|
||||
opacity: 0.5;
|
||||
border: 2px solid #99bbf3;
|
||||
@media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
@media (max-width: 432px) {
|
||||
@media (max-width: 632px) {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
@@ -87,7 +83,7 @@
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 432px) and (max-width: 632px) {
|
||||
@media (max-width: 632px) {
|
||||
font-size: 1.5vw;
|
||||
.desktop-share,
|
||||
.virtual-background-none,
|
||||
@@ -106,29 +102,8 @@
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 432px) {
|
||||
@media (max-width: 360px) {
|
||||
grid-template-columns: auto auto auto;
|
||||
font-size: 1.5vw;
|
||||
.desktop-share,
|
||||
.virtual-background-none,
|
||||
.thumbnail,
|
||||
.blur,
|
||||
.slight-blur {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
.desktop-share-selected,
|
||||
.thumbnail-selected,
|
||||
.none-selected,
|
||||
.blur-selected,
|
||||
.slight-blur-selected {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 320px) {
|
||||
grid-template-columns: auto auto auto;
|
||||
font-size: 1.5vw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +138,7 @@
|
||||
display: none;
|
||||
left: 96;
|
||||
bottom: 51;
|
||||
@media (min-width: 432px) and (max-width: 632px) {
|
||||
@media (max-width: 632px) {
|
||||
left: 51px;
|
||||
}
|
||||
}
|
||||
@@ -196,10 +171,7 @@
|
||||
width: 570px;
|
||||
margin-bottom: 8px;
|
||||
z-index: 2;
|
||||
@media (min-width: 432px) and (max-width: 632px) {
|
||||
max-width: 336;
|
||||
}
|
||||
@media (max-width: 432px) {
|
||||
@media (max-width: 632px) {
|
||||
max-width: 336;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ var interfaceConfig = {
|
||||
RECENT_LIST_ENABLED: true,
|
||||
REMOTE_THUMBNAIL_RATIO: 1, // 1:1
|
||||
|
||||
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
|
||||
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar', 'sounds' ],
|
||||
|
||||
/**
|
||||
* Specify which sharing features should be displayed. If the value is not set
|
||||
@@ -208,13 +208,7 @@ var interfaceConfig = {
|
||||
* DEPRECATED!
|
||||
* This config was moved to config.js as `toolbarButtons`.
|
||||
*/
|
||||
// TOOLBAR_BUTTONS: [
|
||||
// 'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
|
||||
// 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
// 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
// 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
// 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
|
||||
// ],
|
||||
// TOOLBAR_BUTTONS: [],
|
||||
|
||||
TOOLBAR_TIMEOUT: 4000,
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ target 'JitsiMeetSDK' do
|
||||
pod 'react-native-calendar-events', :path => '../node_modules/react-native-calendar-events'
|
||||
pod 'react-native-keep-awake', :path => '../node_modules/react-native-keep-awake'
|
||||
pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo'
|
||||
pod 'react-native-slider', :path => '../node_modules/@react-native-community/slider'
|
||||
pod 'react-native-splash-screen', :path => '../node_modules/react-native-splash-screen'
|
||||
pod 'react-native-webview', :path => '../node_modules/react-native-webview'
|
||||
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
PODS:
|
||||
- AppAuth (1.2.0):
|
||||
- AppAuth/Core (= 1.2.0)
|
||||
- AppAuth/ExternalUserAgent (= 1.2.0)
|
||||
- AppAuth/Core (1.2.0)
|
||||
- AppAuth/ExternalUserAgent (1.2.0)
|
||||
- AppAuth (1.4.0):
|
||||
- AppAuth/Core (= 1.4.0)
|
||||
- AppAuth/ExternalUserAgent (= 1.4.0)
|
||||
- AppAuth/Core (1.4.0)
|
||||
- AppAuth/ExternalUserAgent (1.4.0)
|
||||
- boost-for-react-native (1.63.0)
|
||||
- CocoaLumberjack (3.5.3):
|
||||
- CocoaLumberjack/Core (= 3.5.3)
|
||||
@@ -48,7 +48,7 @@ PODS:
|
||||
- GoogleUtilities/Environment (~> 6.7)
|
||||
- GoogleUtilities/Logger (~> 6.7)
|
||||
- nanopb (~> 1.30906.0)
|
||||
- FirebaseCrashlytics (4.6.1):
|
||||
- FirebaseCrashlytics (4.6.2):
|
||||
- FirebaseCore (~> 6.10)
|
||||
- FirebaseInstallations (~> 1.6)
|
||||
- GoogleDataTransport (~> 7.2)
|
||||
@@ -77,9 +77,9 @@ PODS:
|
||||
- GoogleUtilities/Network (~> 6.7)
|
||||
- "GoogleUtilities/NSData+zlib (~> 6.7)"
|
||||
- nanopb (~> 1.30906.0)
|
||||
- GoogleDataTransport (7.4.0):
|
||||
- GoogleDataTransport (7.5.1):
|
||||
- nanopb (~> 1.30906.0)
|
||||
- GoogleSignIn (5.0.1):
|
||||
- GoogleSignIn (5.0.2):
|
||||
- AppAuth (~> 1.2)
|
||||
- GTMAppAuth (~> 1.0)
|
||||
- GTMSessionFetcher/Core (~> 1.1)
|
||||
@@ -102,21 +102,17 @@ PODS:
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/UserDefaults (6.7.2):
|
||||
- GoogleUtilities/Logger
|
||||
- GTMAppAuth (1.0.0):
|
||||
- AppAuth/Core (~> 1.0)
|
||||
- GTMSessionFetcher (~> 1.1)
|
||||
- GTMSessionFetcher (1.2.2):
|
||||
- GTMSessionFetcher/Full (= 1.2.2)
|
||||
- GTMSessionFetcher/Core (1.2.2)
|
||||
- GTMSessionFetcher/Full (1.2.2):
|
||||
- GTMSessionFetcher/Core (= 1.2.2)
|
||||
- GTMAppAuth (1.2.2):
|
||||
- AppAuth/Core (~> 1.4)
|
||||
- GTMSessionFetcher/Core (~> 1.5)
|
||||
- GTMSessionFetcher/Core (1.6.1)
|
||||
- nanopb (1.30906.0):
|
||||
- nanopb/decode (= 1.30906.0)
|
||||
- nanopb/encode (= 1.30906.0)
|
||||
- nanopb/decode (1.30906.0)
|
||||
- nanopb/encode (1.30906.0)
|
||||
- ObjectiveDropboxOfficial (3.9.4)
|
||||
- PromisesObjC (1.2.10)
|
||||
- PromisesObjC (1.2.12)
|
||||
- RCTRequired (0.61.5-jitsi.2)
|
||||
- RCTTypeSafety (0.61.5-jitsi.2):
|
||||
- FBLazyVector (= 0.61.5-jitsi.2)
|
||||
@@ -288,9 +284,11 @@ PODS:
|
||||
- React
|
||||
- react-native-netinfo (4.1.5):
|
||||
- React
|
||||
- react-native-slider (3.0.3):
|
||||
- React
|
||||
- react-native-splash-screen (3.2.0):
|
||||
- React
|
||||
- react-native-webrtc (1.89.1):
|
||||
- react-native-webrtc (1.89.3):
|
||||
- React-Core
|
||||
- react-native-webview (11.0.2):
|
||||
- React-Core
|
||||
@@ -348,8 +346,8 @@ PODS:
|
||||
- React-jsi (= 0.61.5-jitsi.2)
|
||||
- ReactCommon/jscallinvoker (= 0.61.5-jitsi.2)
|
||||
- ReactCommon/turbomodule/core (= 0.61.5-jitsi.2)
|
||||
- RNCAsyncStorage (1.13.2):
|
||||
- React
|
||||
- RNCAsyncStorage (1.15.5):
|
||||
- React-Core
|
||||
- RNDefaultPreference (1.4.2):
|
||||
- React
|
||||
- RNDeviceInfo (8.0.0):
|
||||
@@ -394,6 +392,7 @@ DEPENDENCIES:
|
||||
- react-native-calendar-events (from `../node_modules/react-native-calendar-events`)
|
||||
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
|
||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
|
||||
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
|
||||
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
@@ -475,6 +474,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-keep-awake"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-slider:
|
||||
:path: "../node_modules/@react-native-community/slider"
|
||||
react-native-splash-screen:
|
||||
:path: "../node_modules/react-native-splash-screen"
|
||||
react-native-webrtc:
|
||||
@@ -519,7 +520,7 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AppAuth: bce82c76043657c99d91e7882e8a9e1a93650cd4
|
||||
AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7
|
||||
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
|
||||
CocoaLumberjack: 2f44e60eb91c176d471fdba43b9e3eae6a721947
|
||||
DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
|
||||
@@ -529,20 +530,20 @@ SPEC CHECKSUMS:
|
||||
FirebaseAnalytics: 5dd088bd2e67bb9d13dbf792d1164ceaf3052193
|
||||
FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd
|
||||
FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1
|
||||
FirebaseCrashlytics: 5777d3462fb8c3ab9e80a2473bd7d667a2e8411c
|
||||
FirebaseCrashlytics: 1a747c9cc084a24dc6d9511c991db1cd078154eb
|
||||
FirebaseDynamicLinks: 6eac37d86910382eafb6315d952cc44c9e176094
|
||||
FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2
|
||||
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
|
||||
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
|
||||
GoogleAppMeasurement: 966e88df9d19c15715137bb2ddaf52373f111436
|
||||
GoogleDataTransport: b7f406340a291370045a270c599e53c6fa6ec20f
|
||||
GoogleSignIn: 3a51b9bb8e48b635fd7f4272cee06ca260345b86
|
||||
GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833
|
||||
GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213
|
||||
GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3
|
||||
GTMAppAuth: 4deac854479704f348309e7b66189e604cf5e01e
|
||||
GTMSessionFetcher: 61bb0f61a4cb560030f1222021178008a5727a23
|
||||
GTMAppAuth: ad5c2b70b9a8689e1a04033c9369c4915bfcbe89
|
||||
GTMSessionFetcher: 36689134877faeb055b27dfa4ccc9ceaa42e029e
|
||||
nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc
|
||||
ObjectiveDropboxOfficial: a5afefc83f6467c42c45f2253f583f2ad1ffc701
|
||||
PromisesObjC: b14b1c6b68e306650688599de8a45e49fae81151
|
||||
PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97
|
||||
RCTRequired: a686731276578c125dff205f08b6ec9cee6ede32
|
||||
RCTTypeSafety: 88e5500e801c00d16a3d1895e3470d13beed6584
|
||||
React: 8b2bcf6a93846e47a7a365a54ec6edeb78b37701
|
||||
@@ -556,8 +557,9 @@ SPEC CHECKSUMS:
|
||||
react-native-calendar-events: 1442fad71a00388f933cfa25512588fec300fcf8
|
||||
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
|
||||
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
|
||||
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
|
||||
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
|
||||
react-native-webrtc: ccb0c21eb4fb04326648fbdb4a5d49977e2cf274
|
||||
react-native-webrtc: 2e8095a43dd3d95ce796d9750280c0f48aadad3d
|
||||
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
|
||||
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
|
||||
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6
|
||||
@@ -569,7 +571,7 @@ SPEC CHECKSUMS:
|
||||
React-RCTText: 4f1b99f228278d2a5e9008eced8dc9c974c4a270
|
||||
React-RCTVibration: c1041024893fdfdb8371e7c720c437751b711676
|
||||
ReactCommon: 18014e1d98dbeb9141e935cfe35fc93bd511ffb6
|
||||
RNCAsyncStorage: bc2f81cc1df90c267ce9ed30bb2dbc93b945a8ee
|
||||
RNCAsyncStorage: 8324611026e8dc3706f829953aa6e3899f581589
|
||||
RNDefaultPreference: 56a405ce61033ac77b95004dccd7ac54c2eb50d1
|
||||
RNDeviceInfo: 72ded653ce636b3f03571e90bed99309a714944e
|
||||
RNGoogleSignin: 39336070b35fc4cea6a98cf111e00480317be0ae
|
||||
@@ -578,6 +580,6 @@ SPEC CHECKSUMS:
|
||||
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
|
||||
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
|
||||
|
||||
PODFILE CHECKSUM: d059cebf82da14a53940a16c24c3330752d4b0c8
|
||||
PODFILE CHECKSUM: 1fa5a1e259f145d32c1ca968b26dac65cff34b49
|
||||
|
||||
COCOAPODS: 1.10.1
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
{
|
||||
"addPeople": {
|
||||
"add": "Convidar",
|
||||
"countryNotSupported": "Ainda não suportamos este destino.",
|
||||
"countryReminder": "Está a ligar de fora dos EUA? Por favor, certifique-se de começar com o código do país!",
|
||||
"disabled": "Você não pode convidar pessoas.",
|
||||
"addContacts": "Convidar os seus contactos",
|
||||
"copyInvite": "Cópia do convite para reunião",
|
||||
"copyLink": "Cópia do link da reunião",
|
||||
"copyStream": "Copiar do link de transmissão em direto",
|
||||
"contacts": "contactos",
|
||||
"countryNotSupported": "Ainda não temos suporte para este destino.",
|
||||
"countryReminder": "Está a telefonar para fora dos EUA? Por favor, certifique-se de que começa com o código do país!",
|
||||
"defaultEmail": "O seu e-mail predefinido",
|
||||
"disabled": "Não pode convidar outras pessoas.",
|
||||
"failedToAdd": "Falha ao adicionar participantes",
|
||||
"footerText": "Digitação está desativada.",
|
||||
"inviteMorePrompt": "Convide mais pessoas",
|
||||
"loading": "A procurar por pessoas e números de telefone",
|
||||
"loadingNumber": "A validar o número de telefone",
|
||||
"loadingPeople": "A procurar pessoas para convidar",
|
||||
"noResults": "Nenhum resultado de busca correspondente",
|
||||
"noValidNumbers": "Por favor, digite um número de telefone",
|
||||
"searchNumbers": "Adicionar números de telefone",
|
||||
"searchPeople": "Pesquisar pessoas",
|
||||
"searchPeopleAndNumbers": "Pesquisar por pessoas ou adicionar os seus números de telefone",
|
||||
"footerText": "A marcação está desactivada.",
|
||||
"googleEmail": "E-mail do Google",
|
||||
"inviteMoreHeader": "Você é o único na reunião",
|
||||
"inviteMoreMailSubject": "Participar na reunião {{appName}}",
|
||||
"inviteMorePrompt": "Convidar mais pessoas",
|
||||
"linkCopied": "Link copiado para a área de transferência",
|
||||
"noResults": "Sem resultados de pesquisa correspondentes",
|
||||
"outlookEmail": "E-mail do Outlook",
|
||||
"phoneNumbers": "números de telefone",
|
||||
"searching": "A pesquisar...",
|
||||
"shareInvite": "Partilhar convite de reunião",
|
||||
"shareLink": "Partilhar o link da reunião para convidar outras pessoas",
|
||||
"shareStream": "Partilhar o link de transmissão em direto",
|
||||
"sipAddresses": "endereços SIP",
|
||||
"telephone": "Telefone: {{number}}",
|
||||
"title": "Convide pessoas para sua reunião"
|
||||
"title": "Convidar pessoas para esta reunião",
|
||||
"yahooEmail": "E-mail do Yahoo"
|
||||
},
|
||||
"audioDevices": {
|
||||
"bluetooth": "Bluetooth",
|
||||
@@ -591,26 +602,39 @@
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
"about": "A integração do calendário {{appName}} é usada para acessar com segurança o seu calendário para que ele possa ler os próximos eventos.",
|
||||
"disconnect": "Desconectar",
|
||||
"microsoftSignIn": "Entrar com Microsoft",
|
||||
"signedIn": "Atualmente acessando eventos do calendário para {{email}}. Clique no botão Desconectar abaixo para parar de acessar os eventos da agenda.",
|
||||
"about": "A integração do calendário {{appName}} é utilizada para aceder com segurança ao seu calendário para que este possa ler os próximos eventos.",
|
||||
"disconnect": "Desligar",
|
||||
"microsoftSignIn": "Iniciar sessão com a Microsoft",
|
||||
"signedIn": "Atualmente a aceder a eventos de calendário por {{email}}. Clique no botão Desconectar abaixo para parar de aceder a eventos de calendário.",
|
||||
"title": "Calendário"
|
||||
},
|
||||
"desktopShareFramerate": "Taxa de fotogramas para partilha do ambiente de trabalho",
|
||||
"desktopShareWarning": "É necessário reiniciar a partilha do ecrã para que as novas definições entrem em vigor.",
|
||||
"desktopShareHighFpsWarning": "Uma taxa de fotogramas mais elevada para a partilha do ambiente de trabalho pode afectar a sua largura de banda. É necessário reiniciar a partilha de ecrã para que as novas definições entrem em vigor.",
|
||||
"devices": "Dispositivos",
|
||||
"followMe": "Todos me seguem",
|
||||
"framesPerSecond": "fotogramas-por-segundo",
|
||||
"incomingMessage": "Receber mensagem",
|
||||
"language": "Idioma",
|
||||
"loggedIn": "Conectado como {{name}}",
|
||||
"loggedIn": "Sessão iniciada como {{name}}",
|
||||
"microphones": "Microfones",
|
||||
"moderator": "Moderador",
|
||||
"more": "Mais",
|
||||
"name": "Nome",
|
||||
"noDevice": "Nenhum",
|
||||
"participantJoined": "Entrar participante",
|
||||
"participantLeft": "Sair participante",
|
||||
"playSounds": "Reproduzir som quando",
|
||||
"sameAsSystem": "O mesmo que o sistema ({{label}})",
|
||||
"selectAudioOutput": "Saída de áudio",
|
||||
"selectCamera": "Câmera",
|
||||
"selectCamera": "Câmara",
|
||||
"selectMic": "Microfone",
|
||||
"startAudioMuted": "Todos iniciam mudos",
|
||||
"startVideoMuted": "Todos iniciam ocultos",
|
||||
"title": "Configurações"
|
||||
"sounds": "Sons",
|
||||
"speakers": "Participantes",
|
||||
"startAudioMuted": "Todos começam com microfone desligado",
|
||||
"startVideoMuted": "Todos começam com câmara desligada",
|
||||
"talkWhileMuted": "se fala e está com microfone desligado",
|
||||
"title": "Definições"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "",
|
||||
@@ -703,7 +727,7 @@
|
||||
"toggleFilmstrip": "Mudar para película de filme",
|
||||
"videomute": "Iniciar / Parar câmara",
|
||||
"videoblur": "Mudar o desfoque de vídeo",
|
||||
"selectBackground": "Selecionar o fundo",
|
||||
"selectBackground": "Selecionar plano de fundo",
|
||||
"expand": "Expandir",
|
||||
"collapse": "Colapsar"
|
||||
},
|
||||
@@ -770,7 +794,7 @@
|
||||
"tileViewToggle": "Mudar para vista em quadrícula",
|
||||
"toggleCamera": "Mudar a câmara",
|
||||
"videomute": "Iniciar / Parar câmara",
|
||||
"selectBackground": "Selecionar o fundo"
|
||||
"selectBackground": "Selecionar plano de fundo"
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "Iniciar/parar legendas",
|
||||
|
||||
@@ -258,10 +258,12 @@
|
||||
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteParticipantButton": "Mute",
|
||||
"muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteParticipantsVideoDialog": "Are you sure you want to turn off this participant's camera? You won't be able to turn the camera back on, but they can turn it back on at any time.",
|
||||
"muteParticipantTitle": "Mute this participant?",
|
||||
"muteParticipantsVideoButton": "Disable camera",
|
||||
"muteParticipantsVideoTitle": "Disable camera of this participant?",
|
||||
"muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
|
||||
"noDropboxToken": "No valid Dropbox token",
|
||||
"Ok": "OK",
|
||||
"password": "Password",
|
||||
"passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.",
|
||||
@@ -418,6 +420,10 @@
|
||||
"invitePhone": "To join by phone instead, tap this: {{number}},,{{conferenceID}}#\n",
|
||||
"invitePhoneAlternatives": "Looking for a different dial-in number?\nSee meeting dial-in numbers: {{url}}\n\n\nIf also dialing-in through a room phone, join without connecting to audio: {{silentUrl}}",
|
||||
"inviteSipEndpoint": "To join using the SIP address, enter this: {{sipUri}}",
|
||||
"inviteTextiOSPersonal": "{{name}} is inviting you to a meeting.",
|
||||
"inviteTextiOSJoinSilent": "If you are dialing-in through a room phone, use this link to join without connecting to audio: {{silentUrl}}.",
|
||||
"inviteTextiOSInviteUrl": "Click the following link to join: {{inviteUrl}}.",
|
||||
"inviteTextiOSPhone": "To join via phone, use this number: {{number}},,{{conferenceID}}#. If you are looking for a different number, this is the full list: {{didUrl}}.",
|
||||
"inviteURLFirstPartGeneral": "You are invited to join a meeting.",
|
||||
"inviteURLFirstPartPersonal": "{{name}} is inviting you to a meeting.\n",
|
||||
"inviteURLSecondPart": "\nJoin the meeting:\n{{url}}\n",
|
||||
@@ -588,16 +594,21 @@
|
||||
"close": "Close",
|
||||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Meeting participants ({{count}})"
|
||||
"participantsList": "Meeting participants ({{count}})",
|
||||
"waitingLobby": "Waiting in lobby ({{count}})"
|
||||
},
|
||||
"actions": {
|
||||
"allow": "Allow attendees to:",
|
||||
"blockEveryoneMicCamera": "Block everyone's mic and camera",
|
||||
"invite": "Invite Someone",
|
||||
"askUnmute": "Ask to unmute",
|
||||
"mute": "Mute",
|
||||
"muteAll": "Mute all",
|
||||
"muteEveryoneElse": "Mute everyone else",
|
||||
"startModeration": "Unmute themselves or start video",
|
||||
"stopEveryonesVideo": "Stop everyone's video",
|
||||
"stopVideo": "Stop video"
|
||||
"stopVideo": "Stop video",
|
||||
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera"
|
||||
}
|
||||
},
|
||||
"passwordSetRemotely": "Set by another participant",
|
||||
@@ -740,6 +751,7 @@
|
||||
"devices": "Devices",
|
||||
"followMe": "Everyone follows me",
|
||||
"framesPerSecond": "frames-per-second",
|
||||
"incomingMessage": "Incoming message",
|
||||
"language": "Language",
|
||||
"loggedIn": "Logged in as {{name}}",
|
||||
"microphones": "Microphones",
|
||||
@@ -747,13 +759,18 @@
|
||||
"more": "More",
|
||||
"name": "Name",
|
||||
"noDevice": "None",
|
||||
"participantJoined": "Participant Joined",
|
||||
"participantLeft": "Participant Left",
|
||||
"playSounds": "Play sound on",
|
||||
"sameAsSystem": "Same as system ({{label}})",
|
||||
"selectAudioOutput": "Audio output",
|
||||
"selectCamera": "Camera",
|
||||
"selectMic": "Microphone",
|
||||
"sounds": "Sounds",
|
||||
"speakers": "Speakers",
|
||||
"startAudioMuted": "Everyone starts muted",
|
||||
"startVideoMuted": "Everyone starts hidden",
|
||||
"talkWhileMuted": "Talk while muted",
|
||||
"title": "Settings"
|
||||
},
|
||||
"settingsView": {
|
||||
@@ -805,6 +822,7 @@
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "Toggle audio only",
|
||||
"audioRoute": "Select the sound device",
|
||||
"boo": "Boo",
|
||||
"callQuality": "Manage video quality",
|
||||
"cc": "Toggle subtitles",
|
||||
"chat": "Open / Close chat",
|
||||
@@ -818,8 +836,8 @@
|
||||
"hangup": "Leave the meeting",
|
||||
"help": "Help",
|
||||
"invite": "Invite people",
|
||||
"joy": "Laughing Crying",
|
||||
"kick": "Kick participant",
|
||||
"laugh": "Laugh",
|
||||
"like": "Thumbs Up",
|
||||
"lobbyButton": "Enable/disable lobby mode",
|
||||
"localRecording": "Toggle local recording controls",
|
||||
@@ -850,7 +868,6 @@
|
||||
"shareYourScreen": "Start / Stop sharing your screen",
|
||||
"shortcuts": "Toggle shortcuts",
|
||||
"show": "Show on stage",
|
||||
"smile": "Smile",
|
||||
"speakerStats": "Toggle speaker statistics",
|
||||
"surprised": "Surprised",
|
||||
"tileView": "Toggle tile view",
|
||||
@@ -869,6 +886,7 @@
|
||||
"audioOnlyOn": "Enable low bandwidth mode",
|
||||
"audioRoute": "Select the sound device",
|
||||
"authenticate": "Authenticate",
|
||||
"boo": "Boo",
|
||||
"callQuality": "Manage video quality",
|
||||
"chat": "Open / Close chat",
|
||||
"clap": "Clap",
|
||||
@@ -887,7 +905,7 @@
|
||||
"hangup": "Leave the meeting",
|
||||
"help": "Help",
|
||||
"invite": "Invite people",
|
||||
"joy": "Joy",
|
||||
"laugh": "Laugh",
|
||||
"like": "Thumbs Up",
|
||||
"lobbyButtonDisable": "Disable lobby mode",
|
||||
"lobbyButtonEnable": "Enable lobby mode",
|
||||
@@ -915,11 +933,11 @@
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Raise / Lower your hand",
|
||||
"raiseYourHand": "Raise your hand",
|
||||
"reactionBoo": "Send boo reaction",
|
||||
"reactionClap": "Send clap reaction",
|
||||
"reactionJoy": "Send joy reaction",
|
||||
"reactionLaugh": "Send laugh reaction",
|
||||
"reactionLike": "Send thumbs up reaction",
|
||||
"reactionParty": "Send party popper reaction",
|
||||
"reactionSmile": "Send smile reaction",
|
||||
"reactionSurprised": "Send surprised reaction",
|
||||
"security": "Security options",
|
||||
"Settings": "Settings",
|
||||
@@ -927,7 +945,6 @@
|
||||
"sharedvideo": "Share video",
|
||||
"shareRoom": "Invite someone",
|
||||
"shortcuts": "View shortcuts",
|
||||
"smile": "Smile",
|
||||
"speakerStats": "Speaker stats",
|
||||
"startScreenSharing": "Start screen sharing",
|
||||
"startSubtitles": "Start subtitles",
|
||||
@@ -1098,6 +1115,7 @@
|
||||
"passwordField": "Enter meeting password",
|
||||
"passwordJoinButton": "Join",
|
||||
"reject": "Reject",
|
||||
"rejectAll": "Reject all",
|
||||
"toggleLabel": "Enable lobby"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ var loggingConfig = {
|
||||
// {@link #defaultLogLevel}:
|
||||
'modules/RTC/TraceablePeerConnection.js': 'info',
|
||||
'modules/statistics/CallStats.js': 'info',
|
||||
'modules/sdp/SDPUtil.js': 'info',
|
||||
'modules/xmpp/JingleSessionPC.js': 'info',
|
||||
'modules/xmpp/strophe.jingle.js': 'info',
|
||||
'modules/xmpp/strophe.util.js': 'log'
|
||||
};
|
||||
|
||||
|
||||
@@ -574,6 +574,12 @@ function initCommands() {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'get-custom-avatar-backgrounds' : {
|
||||
callback({
|
||||
avatarBackgrounds: APP.store.getState()['features/dynamic-branding'].avatarBackgrounds
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
11
modules/API/external/external_api.js
vendored
11
modules/API/external/external_api.js
vendored
@@ -769,6 +769,17 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
return getCurrentDevices(this._transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any custom avatars backgrounds.
|
||||
*
|
||||
* @returns {Promise} - Resolves with the list of custom avatar backgrounds.
|
||||
*/
|
||||
getCustomAvatarBackgrounds() {
|
||||
return this._transport.sendRequest({
|
||||
name: 'get-custom-avatar-backgrounds'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current livestream url.
|
||||
*
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -2913,9 +2913,9 @@
|
||||
"integrity": "sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw=="
|
||||
},
|
||||
"@react-native-async-storage/async-storage": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.13.2.tgz",
|
||||
"integrity": "sha512-isTDvUApRJPVWFxV15yrQSOGqarX7cIedq/y4N5yWSnotf68D9qvDEv1I7rCXhkBDi0u4OJt6GA9dksUT0D3wg==",
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.15.5.tgz",
|
||||
"integrity": "sha512-4AYehLH39B9a8UXCMf3ieOK+G61wGMP72ikx6/XSMA0DUnvx0PgaeaT2Wyt06kTrDTy8edewKnbrbeqwaM50TQ==",
|
||||
"requires": {
|
||||
"deep-assign": "^3.0.0"
|
||||
}
|
||||
@@ -3102,6 +3102,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.1.5.tgz",
|
||||
"integrity": "sha512-lagdZr9UiVAccNXYfTEj+aUcPCx9ykbMe9puffeIyF3JsRuMmlu3BjHYx1klUHX7wNRmFNC8qVP0puxUt1sZ0A=="
|
||||
},
|
||||
"@react-native-community/slider": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-3.0.3.tgz",
|
||||
"integrity": "sha512-8IeHfDwJ9/CTUwFs6x90VlobV3BfuPgNLjTgC6dRZovfCWigaZwVNIFFJnHBakK3pW2xErAPwhdvNR4JeNoYbw=="
|
||||
},
|
||||
"@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
|
||||
@@ -11071,8 +11076,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#89a7e2d9cdd24f48f95f6668ae4d8db1b635cf36",
|
||||
"from": "github:jitsi/lib-jitsi-meet#89a7e2d9cdd24f48f95f6668ae4d8db1b635cf36",
|
||||
"version": "github:jitsi/lib-jitsi-meet#fa834c2923e78fe00cc206b1bc99248874d544c6",
|
||||
"from": "github:jitsi/lib-jitsi-meet#fa834c2923e78fe00cc206b1bc99248874d544c6",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.2",
|
||||
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#5fc4af6dcf8a6e6af9fedbcd654412fd47b1b4ae",
|
||||
@@ -15144,8 +15149,9 @@
|
||||
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "github:react-native-webrtc/react-native-webrtc#510d20dd62c1768885a98f36fde83f9e48a723fa",
|
||||
"from": "github:react-native-webrtc/react-native-webrtc#510d20dd62c1768885a98f36fde83f9e48a723fa",
|
||||
"version": "1.89.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.89.3.tgz",
|
||||
"integrity": "sha512-K6GbLfOeOhJuxgWx8NKoSMnvl6hkqqOCUHZISABOspj00CXNtJgKvFBuCi+fBeNT/RribOODBFxK2sfHqRz7Ww==",
|
||||
"requires": {
|
||||
"base64-js": "^1.1.2",
|
||||
"cross-os": "^1.3.0",
|
||||
|
||||
@@ -36,9 +36,10 @@
|
||||
"@jitsi/js-utils": "1.0.6",
|
||||
"@material-ui/core": "4.11.3",
|
||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||
"@react-native-async-storage/async-storage": "1.13.2",
|
||||
"@react-native-async-storage/async-storage": "1.15.5",
|
||||
"@react-native-community/google-signin": "3.0.1",
|
||||
"@react-native-community/netinfo": "4.1.5",
|
||||
"@react-native-community/slider": "3.0.3",
|
||||
"@svgr/webpack": "4.3.2",
|
||||
"amplitude-js": "8.2.1",
|
||||
"base64-js": "1.3.1",
|
||||
@@ -55,7 +56,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#89a7e2d9cdd24f48f95f6668ae4d8db1b635cf36",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#fa834c2923e78fe00cc206b1bc99248874d544c6",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
@@ -87,7 +88,7 @@
|
||||
"react-native-svg-transformer": "0.14.3",
|
||||
"react-native-url-polyfill": "1.2.0",
|
||||
"react-native-watch-connectivity": "0.4.3",
|
||||
"react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#510d20dd62c1768885a98f36fde83f9e48a723fa",
|
||||
"react-native-webrtc": "1.89.3",
|
||||
"react-native-webview": "11.0.2",
|
||||
"react-native-youtube-iframe": "1.2.3",
|
||||
"react-redux": "7.1.0",
|
||||
|
||||
@@ -21,6 +21,7 @@ const TOOLBAR_TIMEOUT = 4000;
|
||||
*/
|
||||
type State = {
|
||||
avatarURL: string,
|
||||
customAvatarBackgrounds: Array<string>,
|
||||
displayName: string,
|
||||
formattedDisplayName: string,
|
||||
isVideoDisplayed: boolean,
|
||||
@@ -48,6 +49,7 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
|
||||
this.state = {
|
||||
avatarURL: '',
|
||||
customAvatarBackgrounds: [],
|
||||
displayName: '',
|
||||
formattedDisplayName: '',
|
||||
isVideoDisplayed: true,
|
||||
@@ -178,7 +180,14 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderVideoNotAvailableScreen() {
|
||||
const { avatarURL, displayName, formattedDisplayName, isVideoDisplayed, userID } = this.state;
|
||||
const {
|
||||
avatarURL,
|
||||
customAvatarBackgrounds,
|
||||
displayName,
|
||||
formattedDisplayName,
|
||||
isVideoDisplayed,
|
||||
userID
|
||||
} = this.state;
|
||||
|
||||
if (isVideoDisplayed) {
|
||||
return null;
|
||||
@@ -188,7 +197,7 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
<div id = 'videoNotAvailableScreen'>
|
||||
<div id = 'avatarContainer'>
|
||||
<StatelessAvatar
|
||||
color = { getAvatarColor(userID) }
|
||||
color = { getAvatarColor(userID, customAvatarBackgrounds) }
|
||||
id = 'avatar'
|
||||
initials = { getInitials(displayName) }
|
||||
url = { avatarURL } />)
|
||||
@@ -218,6 +227,12 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
window.addEventListener('mousemove', this._mouseMove);
|
||||
|
||||
this._hideToolbarAfterTimeout();
|
||||
api.getCustomAvatarBackgrounds()
|
||||
.then(res =>
|
||||
this.setState({
|
||||
customAvatarBackgrounds: res.avatarBackgrounds || []
|
||||
}))
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -795,6 +795,23 @@ export function createToolbarEvent(buttonName, attributes = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event associated with a reaction button being clicked/pressed.
|
||||
*
|
||||
* @param {string} buttonName - The identifier of the reaction button which was
|
||||
* clicked/pressed.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createReactionMenuEvent(buttonName) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: buttonName,
|
||||
source: 'reaction.button',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that a local track was muted.
|
||||
*
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
parseURIString,
|
||||
toURLString
|
||||
} from '../base/util';
|
||||
import { isVpaasMeeting } from '../billing-counter/functions';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
import { clearNotifications, showNotification } from '../notifications';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { getFeatureFlag } from '../../base/flags/functions';
|
||||
import { Platform } from '../../base/react';
|
||||
import { DimensionsDetector, clientResized } from '../../base/responsive-ui';
|
||||
import { updateSettings } from '../../base/settings';
|
||||
import JitsiThemePaperProvider from '../../base/ui/components/JitsiThemeProvider.native';
|
||||
import logger from '../logger';
|
||||
|
||||
import { AbstractApp } from './AbstractApp';
|
||||
@@ -128,12 +127,10 @@ export class App extends AbstractApp {
|
||||
*/
|
||||
_createMainElement(component, props) {
|
||||
return (
|
||||
<JitsiThemePaperProvider>
|
||||
<DimensionsDetector
|
||||
onDimensionsChanged = { this._onDimensionsChanged }>
|
||||
{ super._createMainElement(component, props) }
|
||||
</DimensionsDetector>
|
||||
</JitsiThemePaperProvider>
|
||||
<DimensionsDetector
|
||||
onDimensionsChanged = { this._onDimensionsChanged }>
|
||||
{ super._createMainElement(component, props) }
|
||||
</DimensionsDetector>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import '../base/sounds/middleware';
|
||||
import '../base/testing/middleware';
|
||||
import '../base/tracks/middleware';
|
||||
import '../base/user-interaction/middleware';
|
||||
import '../billing-counter/middleware';
|
||||
import '../calendar-sync/middleware';
|
||||
import '../chat/middleware';
|
||||
import '../conference/middleware';
|
||||
|
||||
@@ -25,7 +25,6 @@ import '../base/sounds/reducer';
|
||||
import '../base/testing/reducer';
|
||||
import '../base/tracks/reducer';
|
||||
import '../base/user-interaction/reducer';
|
||||
import '../billing-counter/reducer';
|
||||
import '../calendar-sync/reducer';
|
||||
import '../chat/reducer';
|
||||
import '../deep-linking/reducer';
|
||||
@@ -41,6 +40,7 @@ import '../large-video/reducer';
|
||||
import '../lobby/reducer';
|
||||
import '../notifications/reducer';
|
||||
import '../overlay/reducer';
|
||||
import '../participants-pane/reducer';
|
||||
import '../reactions/reducer';
|
||||
import '../recent-list/reducer';
|
||||
import '../recording/reducer';
|
||||
|
||||
@@ -10,6 +10,11 @@ import { StatelessAvatar } from '.';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* Custom avatar backgrounds from branding.
|
||||
*/
|
||||
_customAvatarBackgrounds: Array<string>,
|
||||
|
||||
/**
|
||||
* The string we base the initials on (this is generated from a list of precedences).
|
||||
*/
|
||||
@@ -133,6 +138,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_customAvatarBackgrounds,
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl,
|
||||
className,
|
||||
@@ -172,7 +178,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
|
||||
if (initials) {
|
||||
if (dynamicColor) {
|
||||
avatarProps.color = getAvatarColor(colorBase || _initialsBase);
|
||||
avatarProps.color = getAvatarColor(colorBase || _initialsBase, _customAvatarBackgrounds);
|
||||
}
|
||||
|
||||
avatarProps.initials = initials;
|
||||
@@ -211,6 +217,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const _initialsBase = _participant?.name ?? displayName;
|
||||
|
||||
return {
|
||||
_customAvatarBackgrounds: state['features/dynamic-branding'].avatarBackgrounds,
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl: _participant?.loadableAvatarUrl,
|
||||
colorBase: !colorBase && _participant ? _participant.id : colorBase
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { Icon } from '../../../icons';
|
||||
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
|
||||
|
||||
@@ -31,19 +30,14 @@ type Props = AbstractProps & {
|
||||
/**
|
||||
* TestId of the element, if any.
|
||||
*/
|
||||
testId?: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
testId?: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a stateless avatar component that renders an avatar purely from what gets passed through
|
||||
* props.
|
||||
*/
|
||||
class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
@@ -70,7 +64,7 @@ class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
return (
|
||||
<div className = { this._getBadgeClassName() }>
|
||||
<img
|
||||
alt = { this.props.t('profile.avatar') }
|
||||
alt = 'avatar'
|
||||
className = { this._getAvatarClassName() }
|
||||
data-testid = { this.props.testId }
|
||||
id = { this.props.id }
|
||||
@@ -111,7 +105,7 @@ class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
return (
|
||||
<div className = { this._getBadgeClassName() }>
|
||||
<img
|
||||
alt = { this.props.t('profile.avatar') }
|
||||
alt = 'avatar'
|
||||
className = { this._getAvatarClassName('defaultAvatar') }
|
||||
data-testid = { this.props.testId }
|
||||
id = { this.props.id }
|
||||
@@ -131,7 +125,7 @@ class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
const { size } = this.props;
|
||||
|
||||
return {
|
||||
backgroundColor: color || undefined,
|
||||
background: color || undefined,
|
||||
fontSize: size ? size * 0.5 : '180%',
|
||||
height: size || '100%',
|
||||
width: size || '100%'
|
||||
@@ -165,5 +159,3 @@ class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
|
||||
_isIcon: (?string | ?Object) => boolean
|
||||
}
|
||||
|
||||
export default translate(StatelessAvatar);
|
||||
|
||||
@@ -16,9 +16,13 @@ const AVATAR_OPACITY = 0.4;
|
||||
* Generates the background color of an initials based avatar.
|
||||
*
|
||||
* @param {string?} initials - The initials of the avatar.
|
||||
* @param {Array<strig>} customAvatarBackgrounds - Custom avatar background values.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAvatarColor(initials: ?string) {
|
||||
export function getAvatarColor(initials: ?string, customAvatarBackgrounds: Array<string>) {
|
||||
const hasCustomAvatarBackgronds = customAvatarBackgrounds && customAvatarBackgrounds.length;
|
||||
const colorsBase = hasCustomAvatarBackgronds ? customAvatarBackgrounds : AVATAR_COLORS;
|
||||
|
||||
let colorIndex = 0;
|
||||
|
||||
if (initials) {
|
||||
@@ -28,10 +32,10 @@ export function getAvatarColor(initials: ?string) {
|
||||
nameHash += s.codePointAt(0);
|
||||
}
|
||||
|
||||
colorIndex = nameHash % AVATAR_COLORS.length;
|
||||
colorIndex = nameHash % colorsBase.length;
|
||||
}
|
||||
|
||||
return `rgba(${AVATAR_COLORS[colorIndex]}, ${AVATAR_OPACITY})`;
|
||||
return hasCustomAvatarBackgronds ? colorsBase[colorIndex] : `rgba(${colorsBase[colorIndex]}, ${AVATAR_OPACITY})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -551,7 +551,7 @@ function _updateLocalParticipantInConference({ dispatch, getState }, next, actio
|
||||
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
|
||||
if (conference && participant.id === localParticipant.id) {
|
||||
if (conference && participant.id === localParticipant?.id) {
|
||||
if ('name' in participant) {
|
||||
conference.setDisplayName(participant.name);
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ export default [
|
||||
'disableH264',
|
||||
'disableHPF',
|
||||
'disableInviteFunctions',
|
||||
'disableIncomingMessageSound',
|
||||
'disableJoinLeaveSounds',
|
||||
'disableLocalVideoFlip',
|
||||
'disableNS',
|
||||
@@ -130,6 +131,7 @@ export default [
|
||||
'gatherStats',
|
||||
'googleApiApplicationClientID',
|
||||
'hideConferenceSubject',
|
||||
'hideRecordingLabel',
|
||||
'hideParticipantsStats',
|
||||
'hideConferenceTimer',
|
||||
'hiddenDomain',
|
||||
|
||||
@@ -14,10 +14,35 @@ export const _CONFIG_STORE_PREFIX = 'config.js';
|
||||
* @type Array<string>
|
||||
*/
|
||||
export const TOOLBAR_BUTTONS = [
|
||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
|
||||
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'participants-pane', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone',
|
||||
'security'
|
||||
'camera',
|
||||
'chat',
|
||||
'closedcaptions',
|
||||
'desktop',
|
||||
'download',
|
||||
'embedmeeting',
|
||||
'etherpad',
|
||||
'feedback',
|
||||
'filmstrip',
|
||||
'fullscreen',
|
||||
'hangup',
|
||||
'help',
|
||||
'invite',
|
||||
'livestreaming',
|
||||
'microphone',
|
||||
'mute-everyone',
|
||||
'mute-video-everyone',
|
||||
'participants-pane',
|
||||
'profile',
|
||||
'raisehand',
|
||||
'recording',
|
||||
'security',
|
||||
'select-background',
|
||||
'settings',
|
||||
'shareaudio',
|
||||
'sharedvideo',
|
||||
'shortcuts',
|
||||
'stats',
|
||||
'tileview',
|
||||
'toggle-camera',
|
||||
'videoquality'
|
||||
];
|
||||
|
||||
@@ -60,8 +60,6 @@ export function getRecordingSharingUrl(state: Object) {
|
||||
return state['features/base/config'].recordingSharingUrl;
|
||||
}
|
||||
|
||||
/* eslint-disable max-params, no-shadow */
|
||||
|
||||
/**
|
||||
* Overrides JSON properties in {@code config} and
|
||||
* {@code interfaceConfig} Objects with the values from {@code newConfig}.
|
||||
|
||||
@@ -25,11 +25,21 @@ const GESTURE_SPEED_THRESHOLD = 0.2;
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The height of the screen.
|
||||
*/
|
||||
_height: number,
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of the feature.
|
||||
*/
|
||||
_styles: StyleType,
|
||||
|
||||
/**
|
||||
* Whether to add padding to scroll view.
|
||||
*/
|
||||
addScrollViewPadding?: boolean,
|
||||
|
||||
/**
|
||||
* The children to be displayed within this component.
|
||||
*/
|
||||
@@ -57,9 +67,14 @@ type Props = {
|
||||
renderFooter: ?Function,
|
||||
|
||||
/**
|
||||
* The height of the screen.
|
||||
*/
|
||||
_height: number
|
||||
* Whether to show sliding view or not.
|
||||
*/
|
||||
showSlidingView?: boolean,
|
||||
|
||||
/**
|
||||
* The component's external style
|
||||
*/
|
||||
style: Object
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -68,6 +83,16 @@ type Props = {
|
||||
class BottomSheet extends PureComponent<Props> {
|
||||
panResponder: Object;
|
||||
|
||||
/**
|
||||
* Default values for {@code BottomSheet} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
addScrollViewPadding: true,
|
||||
showSlidingView: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
@@ -90,7 +115,15 @@ class BottomSheet extends PureComponent<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _styles, renderHeader, renderFooter, _height } = this.props;
|
||||
const {
|
||||
_height,
|
||||
_styles,
|
||||
addScrollViewPadding,
|
||||
renderHeader,
|
||||
renderFooter,
|
||||
showSlidingView,
|
||||
style
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SlidingView
|
||||
@@ -98,7 +131,7 @@ class BottomSheet extends PureComponent<Props> {
|
||||
accessibilityViewIsModal = { true }
|
||||
onHide = { this.props.onCancel }
|
||||
position = 'bottom'
|
||||
show = { true }>
|
||||
show = { showSlidingView }>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.sheetContainer }>
|
||||
@@ -110,6 +143,7 @@ class BottomSheet extends PureComponent<Props> {
|
||||
style = { [
|
||||
styles.sheetItemContainer,
|
||||
_styles.sheet,
|
||||
style,
|
||||
{
|
||||
maxHeight: _height - 100
|
||||
}
|
||||
@@ -118,7 +152,7 @@ class BottomSheet extends PureComponent<Props> {
|
||||
<ScrollView
|
||||
bounces = { false }
|
||||
showsVerticalScrollIndicator = { false }
|
||||
style = { styles.scrollView } >
|
||||
style = { addScrollViewPadding && styles.scrollView } >
|
||||
{ this.props.children }
|
||||
</ScrollView>
|
||||
{ renderFooter && renderFooter() }
|
||||
|
||||
@@ -214,3 +214,9 @@ export const VIDEO_SHARE_BUTTON_ENABLED = 'video-share.enabled';
|
||||
* Default: disabled (false).
|
||||
*/
|
||||
export const WELCOME_PAGE_ENABLED = 'welcomepage.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the reactions feature should be enabled.
|
||||
* Default: disabled (false).
|
||||
*/
|
||||
export const REACTIONS_ENABLED = 'reactions.enabled';
|
||||
|
||||
@@ -21,5 +21,5 @@ import './native';
|
||||
// used it from source. As an intermediate step, start using the library
|
||||
// lib-jitsi-meet as a binary on mobile at the time of this writing. In the
|
||||
// future, implement not packaging it in the application bundle.
|
||||
import JitsiMeetJS from 'lib-jitsi-meet/lib-jitsi-meet.min';
|
||||
import JitsiMeetJS from 'lib-jitsi-meet/lib-jitsi-meet.js';
|
||||
export { JitsiMeetJS as default };
|
||||
|
||||
@@ -50,6 +50,11 @@ type Props = {
|
||||
*/
|
||||
headerProps: Object,
|
||||
|
||||
/**
|
||||
* True if the header with navigation should be hidden, false otherwise.
|
||||
*/
|
||||
hideHeaderWithNavigation?: boolean,
|
||||
|
||||
/**
|
||||
* The ID of the modal that is being rendered. This is used to show/hide the modal.
|
||||
*/
|
||||
@@ -78,7 +83,8 @@ type Props = {
|
||||
*/
|
||||
class JitsiModal extends PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
position: 'bottom'
|
||||
position: 'bottom',
|
||||
hideHeaderWithNavigation: false
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -98,7 +104,17 @@ class JitsiModal extends PureComponent<Props> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _headerStyles, _show, _styles, children, footerComponent, headerProps, position, style } = this.props;
|
||||
const {
|
||||
_headerStyles,
|
||||
_show,
|
||||
_styles,
|
||||
children,
|
||||
footerComponent,
|
||||
headerProps,
|
||||
position,
|
||||
hideHeaderWithNavigation,
|
||||
style
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SlidingView
|
||||
@@ -119,6 +135,7 @@ class JitsiModal extends PureComponent<Props> {
|
||||
] }>
|
||||
<HeaderWithNavigation
|
||||
{ ...headerProps }
|
||||
hideHeaderWithNavigation = { hideHeaderWithNavigation }
|
||||
onPressBack = { this._onRequestClose } />
|
||||
<SafeAreaView style = { styles.safeArea }>
|
||||
{ children }
|
||||
|
||||
@@ -371,6 +371,7 @@ function _localParticipantLeft({ dispatch }, next, action) {
|
||||
function _maybePlaySounds({ getState, dispatch }, action) {
|
||||
const state = getState();
|
||||
const { startAudioMuted, disableJoinLeaveSounds } = state['features/base/config'];
|
||||
const { soundsParticipantJoined: joinSound, soundsParticipantLeft: leftSound } = state['features/base/settings'];
|
||||
|
||||
// If we have join/leave sounds disabled, don't play anything.
|
||||
if (disableJoinLeaveSounds) {
|
||||
@@ -387,13 +388,16 @@ function _maybePlaySounds({ getState, dispatch }, action) {
|
||||
const { isReplacing, isReplaced } = action.participant;
|
||||
|
||||
if (action.type === PARTICIPANT_JOINED) {
|
||||
if (!joinSound) {
|
||||
return;
|
||||
}
|
||||
const { presence } = action.participant;
|
||||
|
||||
// The sounds for the poltergeist are handled by features/invite.
|
||||
if (presence !== INVITED && presence !== CALLING && !isReplacing) {
|
||||
dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
|
||||
}
|
||||
} else if (action.type === PARTICIPANT_LEFT && !isReplaced) {
|
||||
} else if (action.type === PARTICIPANT_LEFT && !isReplaced && leftSound) {
|
||||
dispatch(playSound(PARTICIPANT_LEFT_SOUND_ID));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ type Props = {
|
||||
*/
|
||||
headerLabelKey: ?string,
|
||||
|
||||
/**
|
||||
* True if the header with navigation should be hidden, false otherwise.
|
||||
*/
|
||||
hideHeaderWithNavigation?: boolean,
|
||||
|
||||
/**
|
||||
* Callback to be invoked on pressing the back button.
|
||||
*/
|
||||
@@ -48,17 +53,18 @@ class HeaderWithNavigation extends Component<Props> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { onPressBack, onPressForward } = this.props;
|
||||
const { hideHeaderWithNavigation, onPressBack, onPressForward } = this.props;
|
||||
|
||||
return (
|
||||
<Header>
|
||||
{ onPressBack && <BackButton onPress = { onPressBack } /> }
|
||||
<HeaderLabel labelKey = { this.props.headerLabelKey } />
|
||||
{ onPressForward && <ForwardButton
|
||||
disabled = { this.props.forwardDisabled }
|
||||
labelKey = { this.props.forwardLabelKey }
|
||||
onPress = { onPressForward } /> }
|
||||
</Header>
|
||||
!hideHeaderWithNavigation
|
||||
&& <Header>
|
||||
{ onPressBack && <BackButton onPress = { onPressBack } /> }
|
||||
<HeaderLabel labelKey = { this.props.headerLabelKey } />
|
||||
{ onPressForward && <ForwardButton
|
||||
disabled = { this.props.forwardDisabled }
|
||||
labelKey = { this.props.forwardLabelKey }
|
||||
onPress = { onPressForward } /> }
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isVpaasMeeting } from '../../../../billing-counter/functions';
|
||||
import { isVpaasMeeting } from '../../../../jaas/functions';
|
||||
import { translate } from '../../../i18n';
|
||||
import { connect } from '../../../redux';
|
||||
|
||||
|
||||
@@ -3,3 +3,24 @@ export { default as MiddlewareRegistry } from './MiddlewareRegistry';
|
||||
export { default as PersistenceRegistry } from './PersistenceRegistry';
|
||||
export { default as ReducerRegistry } from './ReducerRegistry';
|
||||
export { default as StateListenerRegistry } from './StateListenerRegistry';
|
||||
|
||||
// /**
|
||||
// *
|
||||
// */
|
||||
// MiddlewareRegistry.register(() => next => action => {
|
||||
// if (performance.mark === undefined) {
|
||||
// return next(action);
|
||||
// }
|
||||
// performance.mark(`${action.type}_start`);
|
||||
|
||||
// const result = next(action);
|
||||
|
||||
// performance.mark(`${action.type}_end`);
|
||||
// performance.measure(
|
||||
// `${action.type}`,
|
||||
// `${action.type}_start`,
|
||||
// `${action.type}_end`,
|
||||
// );
|
||||
|
||||
// return result;
|
||||
// });
|
||||
|
||||
@@ -29,12 +29,13 @@ const REDUCED_UI_THRESHOLD = 300;
|
||||
*/
|
||||
export function clientResized(clientWidth: number, clientHeight: number) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
|
||||
let availableWidth = clientWidth;
|
||||
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
const state = getState();
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
|
||||
|
||||
if (isChatOpen) {
|
||||
availableWidth -= CHAT_SIZE;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ const DEFAULT_STATE = {
|
||||
micDeviceId: undefined,
|
||||
serverURL: undefined,
|
||||
hideShareAudioHelper: false,
|
||||
soundsIncomingMessage: true,
|
||||
soundsParticipantJoined: true,
|
||||
soundsParticipantLeft: true,
|
||||
soundsTalkWhileMuted: true,
|
||||
startAudioOnly: false,
|
||||
startWithAudioMuted: false,
|
||||
startWithVideoMuted: false,
|
||||
|
||||
@@ -33,7 +33,8 @@ export const colors = {
|
||||
success04: '#189B55',
|
||||
success05: '#1EC26A',
|
||||
|
||||
warning05: '#F8AE1A'
|
||||
warning05: '#F8AE1A',
|
||||
warning06: '#ED9E1B'
|
||||
};
|
||||
|
||||
// Mapping between the token used and the color
|
||||
@@ -197,20 +198,24 @@ export const colorMap = {
|
||||
success02: 'success05',
|
||||
|
||||
// Color for warning messages applied to icons, borders & backgrounds
|
||||
warning01: 'warning05'
|
||||
warning01: 'warning05',
|
||||
|
||||
// Color for indicating a raised hand
|
||||
warning02: 'warning06'
|
||||
};
|
||||
|
||||
|
||||
export const font = {
|
||||
weightRegular: 400,
|
||||
weightSemiBold: 600
|
||||
weightRegular: '400',
|
||||
weightSemiBold: '600'
|
||||
};
|
||||
|
||||
export const shape = {
|
||||
borderRadius: 6
|
||||
borderRadius: 6,
|
||||
boxShadow: 'inset 0px -1px 0px rgba(255, 255, 255, 0.15)'
|
||||
};
|
||||
|
||||
export const spacing = [ 0, 4, 8, 16, 24, 32, 40, 48, 56 ];
|
||||
export const spacing = [ 0, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80 ];
|
||||
|
||||
export const typography = {
|
||||
labelRegular: {
|
||||
|
||||
@@ -45,17 +45,18 @@ const _URI_PATH_PATTERN = '([^?#]*)';
|
||||
export const URI_PROTOCOL_PATTERN = '^([a-z][a-z0-9\\.\\+-]*:)';
|
||||
|
||||
/**
|
||||
* Excludes/removes certain characters from a specific room (name) which are
|
||||
* incompatible with Jitsi Meet on the client and/or server sides.
|
||||
* Excludes/removes certain characters from a specific path part which are
|
||||
* incompatible with Jitsi Meet on the client and/or server sides. The main
|
||||
* use case for this method is to clean up the room name and the tenant.
|
||||
*
|
||||
* @param {?string} room - The room (name) to fix.
|
||||
* @param {?string} pathPart - The path part to fix.
|
||||
* @private
|
||||
* @returns {?string}
|
||||
*/
|
||||
function _fixRoom(room: ?string) {
|
||||
return room
|
||||
? room.replace(new RegExp(_ROOM_EXCLUDE_PATTERN, 'g'), '')
|
||||
: room;
|
||||
function _fixPathPart(pathPart: ?string) {
|
||||
return pathPart
|
||||
? pathPart.replace(new RegExp(_ROOM_EXCLUDE_PATTERN, 'g'), '')
|
||||
: pathPart;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,6 +336,11 @@ export function parseURIString(uri: ?string) {
|
||||
|
||||
const obj = parseStandardURIString(_fixURIStringScheme(uri));
|
||||
|
||||
// XXX While the components/segments of pathname are URI encoded, Jitsi Meet
|
||||
// on the client and/or server sides still don't support certain characters.
|
||||
obj.pathname = obj.pathname.split('/').map(pathPart => _fixPathPart(pathPart))
|
||||
.join('/');
|
||||
|
||||
// Add the properties that are specific to a Jitsi Meet resource (location)
|
||||
// such as contextRoot, room:
|
||||
|
||||
@@ -344,24 +350,9 @@ export function parseURIString(uri: ?string) {
|
||||
// The room (name) is the last component/segment of pathname.
|
||||
const { pathname } = obj;
|
||||
|
||||
// XXX While the components/segments of pathname are URI encoded, Jitsi Meet
|
||||
// on the client and/or server sides still don't support certain characters.
|
||||
const contextRootEndIndex = pathname.lastIndexOf('/');
|
||||
let room = pathname.substring(contextRootEndIndex + 1) || undefined;
|
||||
|
||||
if (room) {
|
||||
const fixedRoom = _fixRoom(room);
|
||||
|
||||
if (fixedRoom !== room) {
|
||||
room = fixedRoom;
|
||||
|
||||
// XXX Drive fixedRoom into pathname (because room is derived from
|
||||
// pathname).
|
||||
obj.pathname
|
||||
= pathname.substring(0, contextRootEndIndex + 1) + (room || '');
|
||||
}
|
||||
}
|
||||
obj.room = room;
|
||||
obj.room = pathname.substring(contextRootEndIndex + 1) || undefined;
|
||||
|
||||
if (contextRootEndIndex > 1) {
|
||||
// The part of the pathname from the beginning to the room name is the tenant.
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Action used to store the flag signaling the endpoint has been counted.
|
||||
*/
|
||||
export const SET_ENDPOINT_COUNTED = 'SET_ENDPOINT_COUNTED';
|
||||
@@ -1,42 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { SET_ENDPOINT_COUNTED } from './actionTypes';
|
||||
import { extractVpaasTenantFromPath, getBillingId, sendCountRequest } from './functions';
|
||||
|
||||
/**
|
||||
* Sends a billing count request when needed.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function countEndpoint() {
|
||||
return function(dispatch: Function, getState: Function) {
|
||||
const state = getState();
|
||||
const baseUrl = state['features/base/config'].billingCounterUrl;
|
||||
const jwt = state['features/base/jwt'].jwt;
|
||||
const tenant = extractVpaasTenantFromPath(state['features/base/connection'].locationURL.pathname);
|
||||
const shouldSendRequest = Boolean(baseUrl && jwt && tenant);
|
||||
|
||||
if (shouldSendRequest) {
|
||||
const billingId = getBillingId();
|
||||
|
||||
sendCountRequest({
|
||||
baseUrl,
|
||||
billingId,
|
||||
jwt,
|
||||
tenant
|
||||
});
|
||||
dispatch(setEndpointCounted());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to mark the endpoint as counted.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
function setEndpointCounted() {
|
||||
return {
|
||||
type: SET_ENDPOINT_COUNTED
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* The key for the billing id stored in localStorage.
|
||||
*/
|
||||
export const BILLING_ID = 'jitsiMeetId';
|
||||
|
||||
/**
|
||||
* The prefix for the vpaas tenant.
|
||||
*/
|
||||
export const VPAAS_TENANT_PREFIX = 'vpaas-magic-cookie-';
|
||||
@@ -1,119 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { BILLING_ID, VPAAS_TENANT_PREFIX } from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Returns the full vpaas tenant if available, given a path.
|
||||
*
|
||||
* @param {string} path - The meeting url path.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function extractVpaasTenantFromPath(path: string) {
|
||||
const [ , tenant ] = path.split('/');
|
||||
|
||||
if (tenant.startsWith(VPAAS_TENANT_PREFIX)) {
|
||||
return tenant;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the vpaas tenant.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getVpaasTenant(state: Object) {
|
||||
return extractVpaasTenantFromPath(state['features/base/connection'].locationURL.pathname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current meeting is a vpaas one.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @param {boolean} requiredJwt - Whether jwt is required or not.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVpaasMeeting(state: Object, requiredJwt: boolean = true) {
|
||||
const { billingCounterUrl, iAmRecorder, iAmSipGateway } = state['features/base/config'];
|
||||
const { jwt } = state['features/base/jwt'];
|
||||
|
||||
const jwtBoolean = requiredJwt ? Boolean(jwt) : true;
|
||||
|
||||
const isAllowed = iAmRecorder || iAmSipGateway || jwtBoolean;
|
||||
|
||||
return Boolean(
|
||||
billingCounterUrl
|
||||
&& extractVpaasTenantFromPath(
|
||||
state['features/base/connection'].locationURL.pathname)
|
||||
&& isAllowed
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a billing counter request.
|
||||
*
|
||||
* @param {Object} reqData - The request info.
|
||||
* @param {string} reqData.baseUrl - The base url for the request.
|
||||
* @param {string} billingId - The unique id of the client.
|
||||
* @param {string} jwt - The JWT token.
|
||||
* @param {string} tenat - The client tenant.
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function sendCountRequest({ baseUrl, billingId, jwt, tenant }: {
|
||||
baseUrl: string,
|
||||
billingId: string,
|
||||
jwt: string,
|
||||
tenant: string
|
||||
}) {
|
||||
const fullUrl = `${baseUrl}/${encodeURIComponent(tenant)}/${billingId}`;
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${jwt}`
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(fullUrl, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
logger.error('Status error:', res.status);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Could not send request', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored billing id (or generates a new one if none is present).
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBillingId() {
|
||||
let billingId = jitsiLocalStorage.getItem(BILLING_ID);
|
||||
|
||||
if (!billingId) {
|
||||
billingId = uuid.v4();
|
||||
jitsiLocalStorage.setItem(BILLING_ID, billingId);
|
||||
}
|
||||
|
||||
return billingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the billing id for vpaas meetings.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getVpaasBillingId(state: Object) {
|
||||
if (isVpaasMeeting(state)) {
|
||||
return getBillingId();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
SET_ENDPOINT_COUNTED
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
endpointCounted: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions that mutate the billing-counter state
|
||||
*/
|
||||
ReducerRegistry.register(
|
||||
'features/billing-counter', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
|
||||
case SET_ENDPOINT_COUNTED: {
|
||||
return {
|
||||
...state,
|
||||
endpointCounted: true
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -22,11 +22,9 @@ import {
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
import { openDisplayNamePrompt } from '../display-name';
|
||||
import { ADD_REACTIONS_MESSAGE } from '../reactions/actionTypes';
|
||||
import {
|
||||
pushReaction
|
||||
} from '../reactions/actions.any';
|
||||
import { REACTIONS } from '../reactions/constants';
|
||||
import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
|
||||
import { pushReactions } from '../reactions/actions.any';
|
||||
import { getReactionMessageFromBuffer } from '../reactions/functions.any';
|
||||
import { endpointMessageReceived } from '../subtitles';
|
||||
import { showToolbox } from '../toolbox/actions';
|
||||
import {
|
||||
@@ -158,13 +156,13 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case ADD_REACTIONS_MESSAGE: {
|
||||
case ADD_REACTION_MESSAGE: {
|
||||
_handleReceivedMessage(store, {
|
||||
id: localParticipant.id,
|
||||
message: action.message,
|
||||
privateMessage: false,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,8 +210,6 @@ StateListenerRegistry.register(
|
||||
* @returns {void}
|
||||
*/
|
||||
function _addChatMsgListener(conference, store) {
|
||||
const reactions = {};
|
||||
|
||||
if (store.getState()['features/base/config'].iAmRecorder) {
|
||||
// We don't register anything on web if we are in iAmRecorder mode
|
||||
return;
|
||||
@@ -252,30 +248,21 @@ function _addChatMsgListener(conference, store) {
|
||||
const [ { _id }, eventData ] = args;
|
||||
|
||||
if (eventData.name === ENDPOINT_REACTION_NAME) {
|
||||
reactions[_id] = reactions[_id] ?? {
|
||||
timeout: null,
|
||||
message: ''
|
||||
};
|
||||
batch(() => {
|
||||
store.dispatch(pushReaction(eventData.reaction));
|
||||
store.dispatch(setToolboxVisible(true));
|
||||
store.dispatch(setToolboxTimeout(
|
||||
() => store.dispatch(hideToolbox()),
|
||||
5000)
|
||||
);
|
||||
store.dispatch(pushReactions(eventData.reactions));
|
||||
});
|
||||
|
||||
clearTimeout(reactions[_id].timeout);
|
||||
reactions[_id].message = `${reactions[_id].message}${REACTIONS[eventData.reaction].message}`;
|
||||
reactions[_id].timeout = setTimeout(() => {
|
||||
_handleReceivedMessage(store, {
|
||||
id: _id,
|
||||
message: reactions[_id].message,
|
||||
privateMessage: false,
|
||||
timestamp: eventData.timestamp
|
||||
});
|
||||
delete reactions[_id];
|
||||
}, 500);
|
||||
_handleReceivedMessage(store, {
|
||||
id: _id,
|
||||
message: getReactionMessageFromBuffer(eventData.reactions),
|
||||
privateMessage: false,
|
||||
timestamp: eventData.timestamp
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -308,14 +295,20 @@ function _handleChatError({ dispatch }, error) {
|
||||
*
|
||||
* @param {Store} store - The Redux store.
|
||||
* @param {Object} message - The message object.
|
||||
* @param {boolean} shouldPlaySound - Whether or not to play the incoming message sound.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleReceivedMessage({ dispatch, getState }, { id, message, privateMessage, timestamp }) {
|
||||
function _handleReceivedMessage({ dispatch, getState },
|
||||
{ id, message, privateMessage, timestamp },
|
||||
shouldPlaySound = true
|
||||
) {
|
||||
// Logic for all platforms:
|
||||
const state = getState();
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
const { disableIncomingMessageSound } = state['features/base/config'];
|
||||
const { soundsIncomingMessage: soundEnabled } = state['features/base/settings'];
|
||||
|
||||
if (!isChatOpen) {
|
||||
if (!disableIncomingMessageSound && soundEnabled && shouldPlaySound && !isChatOpen) {
|
||||
dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { translate } from '../../base/i18n';
|
||||
import { Icon, IconClose } from '../../base/icons';
|
||||
import { browser } from '../../base/lib-jitsi-meet';
|
||||
import { connect } from '../../base/redux';
|
||||
import { isVpaasMeeting } from '../../billing-counter/functions';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import logger from '../logger';
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
|
||||
import { LargeVideo } from '../../../large-video';
|
||||
import { KnockingParticipantList } from '../../../lobby';
|
||||
import { BackButtonRegistry } from '../../../mobile/back-button';
|
||||
import { ParticipantsPane } from '../../../participants-pane/components/native';
|
||||
import { Captions } from '../../../subtitles';
|
||||
import { setToolboxVisible } from '../../../toolbox/actions';
|
||||
import { Toolbox } from '../../../toolbox/components/native';
|
||||
@@ -71,6 +72,11 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
_fullscreenEnabled: boolean,
|
||||
|
||||
/**
|
||||
* The indicator which determines if the participants pane is open.
|
||||
*/
|
||||
_isParticipantsPaneOpen: boolean,
|
||||
|
||||
/**
|
||||
* The ID of the participant currently on stage (if any)
|
||||
*/
|
||||
@@ -237,6 +243,7 @@ class Conference extends AbstractConference<Props, *> {
|
||||
_renderContent() {
|
||||
const {
|
||||
_connecting,
|
||||
_isParticipantsPaneOpen,
|
||||
_largeVideoParticipantId,
|
||||
_reducedUI,
|
||||
_shouldDisplayTileView
|
||||
@@ -296,11 +303,14 @@ class Conference extends AbstractConference<Props, *> {
|
||||
</SafeAreaView>
|
||||
|
||||
<TestConnectionInfo />
|
||||
|
||||
{ this._renderConferenceNotification() }
|
||||
|
||||
{ this._renderConferenceModals() }
|
||||
|
||||
{_shouldDisplayTileView && <Toolbox />}
|
||||
|
||||
{ _isParticipantsPaneOpen && <ParticipantsPane /> }
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -391,6 +401,7 @@ function _mapStateToProps(state) {
|
||||
membersOnly,
|
||||
leaving
|
||||
} = state['features/base/conference'];
|
||||
const { isOpen } = state['features/participants-pane'];
|
||||
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
|
||||
|
||||
// XXX There is a window of time between the successful establishment of the
|
||||
@@ -412,6 +423,7 @@ function _mapStateToProps(state) {
|
||||
_connecting: Boolean(connecting_),
|
||||
_filmstripVisible: isFilmstripVisible(state),
|
||||
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
|
||||
_isParticipantsPaneOpen: isOpen,
|
||||
_largeVideoParticipantId: state['features/large-video'].participantId,
|
||||
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
|
||||
_reducedUI: reducedUI,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Filmstrip } from '../../../filmstrip';
|
||||
import { CalleeInfoContainer } from '../../../invite';
|
||||
import { LargeVideo } from '../../../large-video';
|
||||
import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
|
||||
import { ParticipantsPane } from '../../../participants-pane/components';
|
||||
import { ParticipantsPane } from '../../../participants-pane/components/web';
|
||||
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
|
||||
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
|
||||
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getParticipantCount } from '../../../base/participants/functions';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { E2EELabel } from '../../../e2ee';
|
||||
import { LocalRecordingLabel } from '../../../local-recording';
|
||||
import { RecordingLabel } from '../../../recording';
|
||||
import { getSessionStatusToShow, RecordingLabel } from '../../../recording';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import { TranscribingLabel } from '../../../transcribing';
|
||||
import { VideoQualityLabel } from '../../../video-quality';
|
||||
@@ -38,6 +38,11 @@ type Props = {
|
||||
*/
|
||||
_hideConferenceTimer: boolean,
|
||||
|
||||
/**
|
||||
* Whether the recording label should be shown or not.
|
||||
*/
|
||||
_hideRecordingLabel: boolean,
|
||||
|
||||
/**
|
||||
* Whether the participant count should be shown or not.
|
||||
*/
|
||||
@@ -52,7 +57,20 @@ type Props = {
|
||||
/**
|
||||
* Indicates whether the component should be visible or not.
|
||||
*/
|
||||
_visible: boolean
|
||||
_visible: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the recording label is visible.
|
||||
*/
|
||||
_recordingLabel: boolean
|
||||
};
|
||||
|
||||
const getLeftMargin = () => {
|
||||
const subjectContainerWidth = document.getElementById('subject-container')?.clientWidth ?? 0;
|
||||
const recContainerWidth = document.getElementById('rec-container')?.clientWidth ?? 0;
|
||||
const subjectDetailsContainer = document.getElementById('subject-details-container')?.clientWidth ?? 0;
|
||||
|
||||
return (subjectContainerWidth - recContainerWidth - subjectDetailsContainer) / 2;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -66,29 +84,46 @@ function ConferenceInfo(props: Props) {
|
||||
_hideConferenceNameAndTimer,
|
||||
_hideConferenceTimer,
|
||||
_showParticipantCount,
|
||||
_hideRecordingLabel,
|
||||
_subject,
|
||||
_fullWidth,
|
||||
_visible
|
||||
_visible,
|
||||
_recordingLabel
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className = { `subject ${_visible ? 'visible' : ''}` }>
|
||||
<div className = { `subject-info-container${_fullWidth ? ' subject-info-container--full-width' : ''}` }>
|
||||
{
|
||||
!_hideConferenceNameAndTimer
|
||||
&& <div className = 'subject-info'>
|
||||
{ _subject && <span className = 'subject-text'>{ _subject }</span>}
|
||||
{ !_hideConferenceTimer && <ConferenceTimer /> }
|
||||
</div>
|
||||
<div className = { `subject ${_recordingLabel ? 'recording' : ''} ${_visible ? 'visible' : ''}` }>
|
||||
<div
|
||||
className = { `subject-info-container${_fullWidth ? ' subject-info-container--full-width' : ''}` }
|
||||
id = 'subject-container'>
|
||||
{!_hideRecordingLabel && <div
|
||||
className = 'show-always'
|
||||
id = 'rec-container'
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
style = {{
|
||||
marginLeft: !_recordingLabel || _visible ? 0 : getLeftMargin()
|
||||
}}>
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
<LocalRecordingLabel />
|
||||
</div>
|
||||
}
|
||||
{ _showParticipantCount && <ParticipantsCount /> }
|
||||
<E2EELabel />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
<LocalRecordingLabel />
|
||||
<TranscribingLabel />
|
||||
<VideoQualityLabel />
|
||||
<InsecureRoomNameLabel />
|
||||
<div
|
||||
className = 'subject-details-container'
|
||||
id = 'subject-details-container'>
|
||||
{
|
||||
!_hideConferenceNameAndTimer
|
||||
&& <div className = 'subject-info'>
|
||||
{ _subject && <span className = 'subject-text'>{ _subject }</span>}
|
||||
{ !_hideConferenceTimer && <ConferenceTimer /> }
|
||||
</div>
|
||||
}
|
||||
{ _showParticipantCount && <ParticipantsCount /> }
|
||||
<E2EELabel />
|
||||
<TranscribingLabel />
|
||||
<VideoQualityLabel />
|
||||
<InsecureRoomNameLabel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -109,16 +144,30 @@ function ConferenceInfo(props: Props) {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const participantCount = getParticipantCount(state);
|
||||
const { hideConferenceTimer, hideConferenceSubject, hideParticipantsStats } = state['features/base/config'];
|
||||
const {
|
||||
hideConferenceTimer,
|
||||
hideConferenceSubject,
|
||||
hideParticipantsStats,
|
||||
hideRecordingLabel
|
||||
} = state['features/base/config'];
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
const fileRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.FILE);
|
||||
const streamRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.STREAM);
|
||||
const isFileRecording = fileRecordingStatus ? fileRecordingStatus !== JitsiRecordingConstants.status.OFF : false;
|
||||
const isStreamRecording = streamRecordingStatus
|
||||
? streamRecordingStatus !== JitsiRecordingConstants.status.OFF : false;
|
||||
const { isEngaged } = state['features/local-recording'];
|
||||
|
||||
return {
|
||||
_hideConferenceNameAndTimer: clientWidth < 300,
|
||||
_hideConferenceTimer: Boolean(hideConferenceTimer),
|
||||
_hideRecordingLabel: hideRecordingLabel,
|
||||
_fullWidth: state['features/video-layout'].tileViewEnabled,
|
||||
_showParticipantCount: participantCount > 2 && !hideParticipantsStats,
|
||||
_subject: hideConferenceSubject ? '' : getConferenceName(state),
|
||||
_visible: isToolboxVisible(state)
|
||||
_visible: isToolboxVisible(state),
|
||||
_recordingLabel: (isFileRecording || isStreamRecording || isEngaged) && !hideRecordingLabel
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { translateToHTML } from '../base/i18n';
|
||||
import { getLocalParticipant } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
import { getBackendSafePath, getJitsiMeetGlobalNS } from '../base/util';
|
||||
import { getVpaasBillingId } from '../billing-counter/functions';
|
||||
import { showWarningNotification } from '../notifications';
|
||||
import { createRnnoiseProcessor } from '../stream-effects/rnnoise';
|
||||
|
||||
@@ -85,7 +84,6 @@ export function getConferenceOptions(stateful) {
|
||||
options.applicationName = getName();
|
||||
options.getWiFiStatsMethod = getWiFiStatsMethod;
|
||||
options.createVADProcessor = createRnnoiseProcessor;
|
||||
options.billingId = getVpaasBillingId(state);
|
||||
|
||||
// Disable CallStats, if requessted.
|
||||
if (options.disableThirdPartyRequests) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { isMobileBrowser } from '../base/environment/utils';
|
||||
import { Platform } from '../base/react';
|
||||
import { URI_PROTOCOL_PATTERN } from '../base/util';
|
||||
import { isVpaasMeeting } from '../billing-counter/functions';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
|
||||
import {
|
||||
DeepLinkingDesktopPage,
|
||||
@@ -58,7 +58,7 @@ export function getDeepLinkingPage(state) {
|
||||
if (launchInWeb
|
||||
|| !room
|
||||
|| state['features/base/config'].disableDeepLinking
|
||||
|| (isVpaasMeeting(state) && !appScheme)) {
|
||||
|| (isVpaasMeeting(state) && (!appScheme || appScheme === 'com.8x8.meet'))) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The name to be displayed within the badge.
|
||||
*/
|
||||
name: string
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
return {
|
||||
badge: {
|
||||
background: 'rgba(0, 0, 0, 0.6)',
|
||||
borderRadius: '3px',
|
||||
color: theme.palette.text01,
|
||||
maxWidth: '50%',
|
||||
overflow: 'hidden',
|
||||
padding: '2px 16px',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component that displays a name badge.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
const DisplayNameBadge = ({ name }: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.badge }>
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisplayNameBadge;
|
||||
@@ -0,0 +1,60 @@
|
||||
// @flow
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import { getLargeVideoParticipant } from '../../../large-video/functions';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import { isLayoutTileView } from '../../../video-layout';
|
||||
|
||||
import DisplayNameBadge from './DisplayNameBadge';
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
return {
|
||||
badgeContainer: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginBottom: theme.spacing(2),
|
||||
transition: 'margin-bottom 0.3s'
|
||||
},
|
||||
containerElevated: {
|
||||
marginBottom: theme.spacing(7)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component that renders the dominant speaker's name as a badge above the toolbar in stage view.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
const DominantSpeakerName = () => {
|
||||
const classes = useStyles();
|
||||
const largeVideoParticipant = useSelector(getLargeVideoParticipant);
|
||||
const nameToDisplay = largeVideoParticipant?.name;
|
||||
const selectedId = largeVideoParticipant?.id;
|
||||
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const localId = localParticipant?.id;
|
||||
|
||||
const isTileView = useSelector(isLayoutTileView);
|
||||
const toolboxVisible = useSelector(isToolboxVisible);
|
||||
|
||||
if (nameToDisplay && selectedId !== localId && !isTileView) {
|
||||
return (
|
||||
<div
|
||||
className = { `${classes.badgeContainer}${toolboxVisible ? '' : ` ${classes.containerElevated}`}` }>
|
||||
<DisplayNameBadge name = { nameToDisplay } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default DominantSpeakerName;
|
||||
@@ -3,3 +3,4 @@
|
||||
export { default as DisplayName } from './DisplayName';
|
||||
export { default as DisplayNameLabel } from './DisplayNameLabel';
|
||||
export { default as DisplayNamePrompt } from './DisplayNamePrompt';
|
||||
export { default as DominantSpeakerName } from './DominantSpeakerName';
|
||||
|
||||
@@ -15,6 +15,15 @@ import {
|
||||
const STORE_NAME = 'features/dynamic-branding';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
|
||||
/**
|
||||
* The pool of avatar backgrounds.
|
||||
*
|
||||
* @public
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
avatarBackgrounds: [],
|
||||
|
||||
/**
|
||||
* The custom background color for the LargeVideo.
|
||||
*
|
||||
@@ -112,10 +121,12 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
|
||||
didPageUrl,
|
||||
inviteDomain,
|
||||
logoClickUrl,
|
||||
logoImageUrl
|
||||
logoImageUrl,
|
||||
avatarBackgrounds
|
||||
} = action.value;
|
||||
|
||||
return {
|
||||
avatarBackgrounds,
|
||||
backgroundColor,
|
||||
backgroundImageUrl,
|
||||
defaultBranding,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../base/media';
|
||||
import { getLocalParticipant } from '../base/participants';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../base/tracks';
|
||||
import { appendSuffix } from '../display-name';
|
||||
import { shouldDisplayTileView } from '../video-layout';
|
||||
|
||||
@@ -45,12 +43,7 @@ StateListenerRegistry.register(
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/large-video'].participantId,
|
||||
/* listener */ (participantId, store) => {
|
||||
const videoTrack = getTrackByMediaTypeAndParticipant(
|
||||
store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
|
||||
|
||||
if (videoTrack && videoTrack.videoType === VIDEO_TYPE.CAMERA) {
|
||||
APP.API.notifyOnStageParticipantChanged(participantId);
|
||||
}
|
||||
/* listener */ participantId => {
|
||||
APP.API.notifyOnStageParticipantChanged(participantId);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { Dispatch } from 'redux';
|
||||
|
||||
import { FEEDBACK_REQUEST_IN_PROGRESS } from '../../../modules/UI/UIErrors';
|
||||
import { openDialog } from '../base/dialog';
|
||||
import { isVpaasMeeting } from '../billing-counter/functions';
|
||||
import { extractFqnFromPath } from '../dynamic-branding/functions';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
|
||||
import {
|
||||
CANCEL_FEEDBACK,
|
||||
|
||||
59
react/features/filmstrip/actions.any.js
Normal file
59
react/features/filmstrip/actions.any.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
SET_FILMSTRIP_ENABLED,
|
||||
SET_FILMSTRIP_VISIBLE,
|
||||
SET_VISIBLE_REMOTE_PARTICIPANTS
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Sets whether the filmstrip is enabled.
|
||||
*
|
||||
* @param {boolean} enabled - Whether the filmstrip is enabled.
|
||||
* @returns {{
|
||||
* type: SET_FILMSTRIP_ENABLED,
|
||||
* enabled: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setFilmstripEnabled(enabled: boolean) {
|
||||
return {
|
||||
type: SET_FILMSTRIP_ENABLED,
|
||||
enabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the filmstrip is visible.
|
||||
*
|
||||
* @param {boolean} visible - Whether the filmstrip is visible.
|
||||
* @returns {{
|
||||
* type: SET_FILMSTRIP_VISIBLE,
|
||||
* visible: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setFilmstripVisible(visible: boolean) {
|
||||
return {
|
||||
type: SET_FILMSTRIP_VISIBLE,
|
||||
visible
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote
|
||||
* participants array.
|
||||
*
|
||||
* @param {number} startIndex - The start index from the remote participants array.
|
||||
* @param {number} endIndex - The end index from the remote participants array.
|
||||
* @returns {{
|
||||
* type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
* startIndex: number,
|
||||
* endIndex: number
|
||||
* }}
|
||||
*/
|
||||
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
|
||||
return {
|
||||
type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
startIndex,
|
||||
endIndex
|
||||
};
|
||||
}
|
||||
@@ -1,42 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
SET_FILMSTRIP_ENABLED,
|
||||
SET_FILMSTRIP_VISIBLE,
|
||||
SET_TILE_VIEW_DIMENSIONS
|
||||
} from './actionTypes';
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
|
||||
/**
|
||||
* Sets whether the filmstrip is enabled.
|
||||
*
|
||||
* @param {boolean} enabled - Whether the filmstrip is enabled.
|
||||
* @returns {{
|
||||
* type: SET_FILMSTRIP_ENABLED,
|
||||
* enabled: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setFilmstripEnabled(enabled: boolean) {
|
||||
return {
|
||||
type: SET_FILMSTRIP_ENABLED,
|
||||
enabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the filmstrip is visible.
|
||||
*
|
||||
* @param {boolean} visible - Whether the filmstrip is visible.
|
||||
* @returns {{
|
||||
* type: SET_FILMSTRIP_VISIBLE,
|
||||
* visible: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setFilmstripVisible(visible: boolean) {
|
||||
return {
|
||||
type: SET_FILMSTRIP_VISIBLE,
|
||||
visible
|
||||
};
|
||||
}
|
||||
import { SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
|
||||
import { SQUARE_TILE_ASPECT_RATIO, TILE_MARGIN } from './constants';
|
||||
import { getColumnCount } from './functions.native';
|
||||
|
||||
/**
|
||||
* Sets the dimensions of the tile view grid. The action is only partially implemented on native as not all
|
||||
@@ -52,11 +20,40 @@ export function setFilmstripVisible(visible: boolean) {
|
||||
* dimensions: Object
|
||||
* }}
|
||||
*/
|
||||
export function setTileViewDimensions({ thumbnailSize }: Object) {
|
||||
return {
|
||||
type: SET_TILE_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
thumbnailSize
|
||||
export function setTileViewDimensions() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
|
||||
const columns = getColumnCount(state);
|
||||
const heightToUse = height - (TILE_MARGIN * 2);
|
||||
const widthToUse = width - (TILE_MARGIN * 2);
|
||||
let tileWidth;
|
||||
|
||||
// If there is going to be at least two rows, ensure that at least two
|
||||
// rows display fully on screen.
|
||||
if (participantCount / columns > 1) {
|
||||
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
|
||||
} else {
|
||||
tileWidth = Math.min(widthToUse / columns, heightToUse);
|
||||
}
|
||||
|
||||
const tileHeight = Math.floor(tileWidth / SQUARE_TILE_ASPECT_RATIO);
|
||||
|
||||
tileWidth = Math.floor(tileWidth);
|
||||
|
||||
|
||||
dispatch({
|
||||
type: SET_TILE_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
columns,
|
||||
thumbnailSize: {
|
||||
height: tileHeight,
|
||||
width: tileWidth
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
SET_TILE_VIEW_DIMENSIONS,
|
||||
SET_VERTICAL_VIEW_DIMENSIONS,
|
||||
SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
SET_VOLUME
|
||||
} from './actionTypes';
|
||||
import {
|
||||
@@ -137,7 +136,7 @@ export function clickOnVideo(n: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume for a thumnail's audio.
|
||||
* Sets the volume for a thumbnail's audio.
|
||||
*
|
||||
* @param {string} participantId - The participant ID asociated with the audio.
|
||||
* @param {string} volume - The volume level.
|
||||
@@ -155,24 +154,4 @@ export function setVolume(participantId: string, volume: number) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote
|
||||
* participants array.
|
||||
*
|
||||
* @param {number} startIndex - The start index from the remote participants array.
|
||||
* @param {number} endIndex - The end index from the remote participants array.
|
||||
* @returns {{
|
||||
* type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
* startIndex: number,
|
||||
* endIndex: number
|
||||
* }}
|
||||
*/
|
||||
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
|
||||
return {
|
||||
type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
startIndex,
|
||||
endIndex
|
||||
};
|
||||
}
|
||||
|
||||
export * from './actions.native';
|
||||
export * from './actions.any';
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { SafeAreaView, ScrollView } from 'react-native';
|
||||
import { FlatList, SafeAreaView, ScrollView } from 'react-native';
|
||||
|
||||
import { Platform } from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { setVisibleRemoteParticipants } from '../../actions';
|
||||
import { isFilmstripVisible } from '../../functions';
|
||||
|
||||
|
||||
import LocalThumbnail from './LocalThumbnail';
|
||||
import Thumbnail from './Thumbnail';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* Filmstrip component's property types.
|
||||
*/
|
||||
@@ -30,7 +33,12 @@ type Props = {
|
||||
/**
|
||||
* The indicator which determines whether the filmstrip is visible.
|
||||
*/
|
||||
_visible: boolean
|
||||
_visible: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to trigger state changes in Redux.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -74,6 +82,65 @@ class Filmstrip extends Component<Props> {
|
||||
this._separateLocalThumbnail = Platform.OS !== 'android';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} item
|
||||
* @returns
|
||||
*/
|
||||
_keyExtractor(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} data
|
||||
* @param {*} index
|
||||
* @returns
|
||||
*/
|
||||
_getItemLayout(data, index) {
|
||||
const { _aspectRatio } = this.props;
|
||||
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
|
||||
const length = isNarrowAspectRatio ? styles.thumbnail.width : styles.thumbnail.height;
|
||||
|
||||
return {
|
||||
length,
|
||||
offset: length * index,
|
||||
index
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler for visible items changes.
|
||||
*
|
||||
* @param {Object} data - The visible items data.
|
||||
* @param {Array<Object>} data.viewableItems - The visible items array.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onViewableItemsChanged({ viewableItems = 0 }) {
|
||||
console.log(`visisble indexes ${JSON.stringify((viewableItems || []).map(i => i.index))}`);
|
||||
const indexArray = viewableItems.map(i => i.index);
|
||||
|
||||
this.props.dispatch(setVisibleRemoteParticipants(Math.min(indexArray), Math.max(indexArray)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates React Elements to display each participant in a thumbnail. Each
|
||||
* tile will be.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
*/
|
||||
_renderThumbnail({ item, index /*, separators */ }) {
|
||||
console.log(`1rendering thumbnail with index ${index}`);
|
||||
|
||||
return (
|
||||
<Thumbnail
|
||||
index = { index }
|
||||
key = { item }
|
||||
participantID = { item } />)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -89,6 +156,8 @@ class Filmstrip extends Component<Props> {
|
||||
|
||||
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
|
||||
const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide;
|
||||
const participants = this._sort(_participants, isNarrowAspectRatio);
|
||||
const initialRowsToRender = Math.ceil(_height / _thumbnailHeight);
|
||||
|
||||
return (
|
||||
<SafeAreaView style = { filmstripStyle }>
|
||||
@@ -97,6 +166,18 @@ class Filmstrip extends Component<Props> {
|
||||
&& !isNarrowAspectRatio
|
||||
&& <LocalThumbnail />
|
||||
}
|
||||
<FlatList
|
||||
data = { participants }
|
||||
getItemLayout = { this._getItemLayout }
|
||||
horizontal = { isNarrowAspectRatio }
|
||||
initialNumToRender = { initialRowsToRender }
|
||||
key = { isNarrowAspectRatio ? 'narrow' : 'wide' }
|
||||
keyExtractor = { this._keyExtractor }
|
||||
onViewableItemsChanged = { this._onViewableItemsChanged }
|
||||
renderItem = { this._renderThumbnail }
|
||||
style = { styles.scrollView }
|
||||
viewabilityConfig = { this.viewabilityConfig }
|
||||
windowSize = { 2 } />
|
||||
<ScrollView
|
||||
horizontal = { isNarrowAspectRatio }
|
||||
showsHorizontalScrollIndicator = { false }
|
||||
@@ -128,6 +209,40 @@ class Filmstrip extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _columns, _height, _thumbnailHeight, _width, onClick } = this.props;
|
||||
|
||||
const participants = this._getSortedParticipants();
|
||||
const initialRowsToRender = Math.ceil(_height / _thumbnailHeight);
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress = { onClick }>
|
||||
<FlatList
|
||||
contentContainerStyle = { styles.tileView }
|
||||
data = { participants }
|
||||
horizontal = { false }
|
||||
initialNumToRender = { initialRowsToRender }
|
||||
key = { _columns }
|
||||
keyExtractor = { this._keyExtractor }
|
||||
numColumns = { _columns }
|
||||
onViewableItemsChanged = { this._onViewableItemsChanged }
|
||||
renderItem = { this._renderThumbnail }
|
||||
style = {{
|
||||
height: _height,
|
||||
width: _width
|
||||
}}
|
||||
viewabilityConfig = { this.viewabilityConfig }
|
||||
windowSize = { 2 } />
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts a specific array of {@code Participant}s in display order.
|
||||
*
|
||||
|
||||
@@ -32,6 +32,8 @@ import RaisedHandIndicator from './RaisedHandIndicator';
|
||||
import ScreenShareIndicator from './ScreenShareIndicator';
|
||||
import VideoMutedIndicator from './VideoMutedIndicator';
|
||||
import styles, { AVATAR_SIZE } from './styles';
|
||||
import { PureComponent } from 'react';
|
||||
import { SQUARE_TILE_ASPECT_RATIO } from '../../constants';
|
||||
|
||||
/**
|
||||
* Thumbnail component's property types.
|
||||
@@ -112,103 +114,149 @@ type Props = {
|
||||
* @param {Props} props - Properties passed to this functional component.
|
||||
* @returns {Component} - A React component.
|
||||
*/
|
||||
function Thumbnail(props: Props) {
|
||||
const {
|
||||
_audioMuted: audioMuted,
|
||||
_largeVideo: largeVideo,
|
||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||
_renderModeratorIndicator: renderModeratorIndicator,
|
||||
_participant: participant,
|
||||
_styles,
|
||||
_videoTrack: videoTrack,
|
||||
dispatch,
|
||||
disableTint,
|
||||
renderDisplayName,
|
||||
tileView
|
||||
} = props;
|
||||
class Thumbnail extends PureComponent<Props> {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onThumbnailLongPress = this._onThumbnailLongPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Thumbnail click handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { _participantId, _pinned, dispatch, tileView } = this.props;
|
||||
|
||||
const participantId = participant.id;
|
||||
const participantInLargeVideo
|
||||
= participantId === largeVideo.participantId;
|
||||
const videoMuted = !videoTrack || videoTrack.muted;
|
||||
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
|
||||
const onClick = useCallback(() => {
|
||||
if (tileView) {
|
||||
dispatch(toggleToolboxVisible());
|
||||
} else {
|
||||
dispatch(pinParticipant(participant.pinned ? null : participant.id));
|
||||
dispatch(pinParticipant(_pinned ? null : _participantId));
|
||||
}
|
||||
}, [ participant, tileView, dispatch ]);
|
||||
const onThumbnailLongPress = useCallback(() => {
|
||||
if (participant.local) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Thumbnail long press handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onThumbnailLongPress() {
|
||||
const { _participantId, _local, dispatch } = this.props;
|
||||
|
||||
if (_local) {
|
||||
dispatch(openDialog(ConnectionStatusComponent, {
|
||||
participantID: participant.id
|
||||
participantID: _participantId
|
||||
}));
|
||||
} else {
|
||||
dispatch(openDialog(RemoteVideoMenu, {
|
||||
participant
|
||||
participantId: _participantId
|
||||
}));
|
||||
}
|
||||
}, [ participant, dispatch ]);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { onClick }
|
||||
onLongPress = { onThumbnailLongPress }
|
||||
style = { [
|
||||
styles.thumbnail,
|
||||
participant.pinned && !tileView
|
||||
? _styles.thumbnailPinned : null,
|
||||
props.styleOverrides || null
|
||||
] }
|
||||
touchFeedback = { false }>
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_audioMuted: audioMuted,
|
||||
_isScreenShare: isScreenShare,
|
||||
_isFakeParticipant,
|
||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||
_renderModeratorIndicator: renderModeratorIndicator,
|
||||
_participantId: participantId,
|
||||
_participantInLargeVideo: participantInLargeVideo,
|
||||
_pinned,
|
||||
_styles,
|
||||
_videoMuted: videoMuted,
|
||||
disableTint,
|
||||
height,
|
||||
index,
|
||||
renderDisplayName,
|
||||
tileView
|
||||
} = this.props;
|
||||
|
||||
<ParticipantView
|
||||
avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
|
||||
disableVideo = { isScreenShare || participant.isFakeParticipant }
|
||||
participantId = { participantId }
|
||||
style = { _styles.participantViewStyle }
|
||||
tintEnabled = { participantInLargeVideo && !disableTint }
|
||||
tintStyle = { _styles.activeThumbnailTint }
|
||||
zOrder = { 1 } />
|
||||
const styleOverrides = tileView ? {
|
||||
aspectRatio: SQUARE_TILE_ASPECT_RATIO,
|
||||
flex: 0,
|
||||
height,
|
||||
maxHeight: null,
|
||||
maxWidth: null,
|
||||
width: null
|
||||
} : null;
|
||||
|
||||
{ renderDisplayName && <Container style = { styles.displayNameContainer }>
|
||||
<DisplayNameLabel participantId = { participantId } />
|
||||
</Container> }
|
||||
console.log(`rendering thumbnail with index ${index}`);
|
||||
|
||||
{ renderModeratorIndicator
|
||||
&& <View style = { styles.moderatorIndicatorContainer }>
|
||||
<ModeratorIndicator />
|
||||
</View>}
|
||||
|
||||
{ !participant.isFakeParticipant && <View
|
||||
return (
|
||||
<Container
|
||||
onClick = { this._onClick }
|
||||
onLongPress = { this._onThumbnailLongPress }
|
||||
style = { [
|
||||
styles.thumbnailTopIndicatorContainer,
|
||||
styles.thumbnailTopLeftIndicatorContainer
|
||||
] }>
|
||||
<RaisedHandIndicator participantId = { participant.id } />
|
||||
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
|
||||
</View> }
|
||||
|
||||
{ !participant.isFakeParticipant && <View
|
||||
style = { [
|
||||
styles.thumbnailTopIndicatorContainer,
|
||||
styles.thumbnailTopRightIndicatorContainer
|
||||
] }>
|
||||
<ConnectionIndicator participantId = { participant.id } />
|
||||
</View> }
|
||||
|
||||
{ !participant.isFakeParticipant && <Container style = { styles.thumbnailIndicatorContainer }>
|
||||
{ audioMuted
|
||||
&& <AudioMutedIndicator /> }
|
||||
{ videoMuted
|
||||
&& <VideoMutedIndicator /> }
|
||||
{ isScreenShare
|
||||
&& <ScreenShareIndicator /> }
|
||||
</Container> }
|
||||
|
||||
</Container>
|
||||
);
|
||||
styles.thumbnail,
|
||||
_pinned && !tileView ? _styles.thumbnailPinned : null,
|
||||
styleOverrides
|
||||
] }
|
||||
touchFeedback = { false }>
|
||||
<ParticipantView
|
||||
avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
|
||||
disableVideo = { isScreenShare || _isFakeParticipant }
|
||||
participantId = { participantId }
|
||||
style = { _styles.participantViewStyle }
|
||||
tintEnabled = { participantInLargeVideo && !disableTint }
|
||||
tintStyle = { _styles.activeThumbnailTint }
|
||||
zOrder = { 1 } />
|
||||
{
|
||||
renderDisplayName
|
||||
&& <Container style = { styles.displayNameContainer }>
|
||||
<DisplayNameLabel participantId = { participantId } />
|
||||
</Container>
|
||||
}
|
||||
{ renderModeratorIndicator
|
||||
&& <View style = { styles.moderatorIndicatorContainer }>
|
||||
<ModeratorIndicator />
|
||||
</View>
|
||||
}
|
||||
{
|
||||
!_isFakeParticipant
|
||||
&& <View
|
||||
style = { [
|
||||
styles.thumbnailTopIndicatorContainer,
|
||||
styles.thumbnailTopLeftIndicatorContainer
|
||||
] }>
|
||||
<RaisedHandIndicator participantId = { participantId } />
|
||||
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
|
||||
</View>
|
||||
}
|
||||
{
|
||||
!_isFakeParticipant
|
||||
&& <View
|
||||
style = { [
|
||||
styles.thumbnailTopIndicatorContainer,
|
||||
styles.thumbnailTopRightIndicatorContainer
|
||||
] }>
|
||||
<ConnectionIndicator participantId = { participantId } />
|
||||
</View>
|
||||
}
|
||||
{
|
||||
!_isFakeParticipant
|
||||
&& <Container style = { styles.thumbnailIndicatorContainer }>
|
||||
{ audioMuted && <AudioMutedIndicator /> }
|
||||
{ videoMuted && <VideoMutedIndicator /> }
|
||||
{ isScreenShare && <ScreenShareIndicator /> }
|
||||
</Container>
|
||||
}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,20 +279,27 @@ function _mapStateToProps(state, ownProps) {
|
||||
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
|
||||
const videoTrack
|
||||
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
|
||||
const videoMuted = !videoTrack || videoTrack.muted;
|
||||
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
|
||||
const participantCount = getParticipantCount(state);
|
||||
const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
|
||||
const _isEveryoneModerator = isEveryoneModerator(state);
|
||||
const renderModeratorIndicator = !_isEveryoneModerator
|
||||
&& participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
|
||||
const participantInLargeVideo = id === largeVideo.participantId;
|
||||
|
||||
return {
|
||||
_audioMuted: audioTrack?.muted ?? true,
|
||||
_largeVideo: largeVideo,
|
||||
_participant: participant,
|
||||
_isScreenShare: isScreenShare,
|
||||
_isFakeParticipant: participant.isFakeParticipant,
|
||||
_local: participant.local,
|
||||
_participantInLargeVideo: participantInLargeVideo,
|
||||
_participantId: id,
|
||||
_pinned: participant.pinned,
|
||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||
_renderModeratorIndicator: renderModeratorIndicator,
|
||||
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
|
||||
_videoTrack: videoTrack
|
||||
_videoMuted: videoMuted
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
FlatList,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -10,12 +10,11 @@ import type { Dispatch } from 'redux';
|
||||
|
||||
import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { setTileViewDimensions } from '../../actions.native';
|
||||
import { SQUARE_TILE_ASPECT_RATIO } from '../../constants';
|
||||
|
||||
import Thumbnail from './Thumbnail';
|
||||
import styles from './styles';
|
||||
|
||||
import { setVisibleRemoteParticipants } from '../../actions.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link TileView}.
|
||||
@@ -27,6 +26,8 @@ type Props = {
|
||||
*/
|
||||
_aspectRatio: Symbol,
|
||||
|
||||
_columns: number,
|
||||
|
||||
/**
|
||||
* Application's viewport height.
|
||||
*/
|
||||
@@ -47,6 +48,8 @@ type Props = {
|
||||
*/
|
||||
_remoteParticipants: Array<string>,
|
||||
|
||||
_thumbnailHeight: number,
|
||||
|
||||
/**
|
||||
* Application's viewport height.
|
||||
*/
|
||||
@@ -63,23 +66,6 @@ type Props = {
|
||||
onClick: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The margin for each side of the tile view. Taken away from the available
|
||||
* height and width for the tile container to display in.
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
const MARGIN = 10;
|
||||
|
||||
/**
|
||||
* The aspect ratio the tiles should display in.
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
const TILE_ASPECT_RATIO = 1;
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays thumbnails in a two
|
||||
* dimensional grid.
|
||||
@@ -87,22 +73,63 @@ const TILE_ASPECT_RATIO = 1;
|
||||
* @extends Component
|
||||
*/
|
||||
class TileView extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @param {*} props
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateReceiverQuality();
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._keyExtractor = this._keyExtractor.bind(this);
|
||||
this._getItemLayout = this._getItemLayout.bind(this);
|
||||
this._onViewableItemsChanged = this._onViewableItemsChanged.bind(this);
|
||||
this._getSortedParticipants = this._getSortedParticipants.bind(this);
|
||||
this._renderThumbnail = this._renderThumbnail.bind(this);
|
||||
this.viewabilityConfig = {
|
||||
itemVisiblePercentThreshold: 30
|
||||
};
|
||||
this._flatListStyles = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @param {*} item
|
||||
* @returns
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
this._updateReceiverQuality();
|
||||
_keyExtractor(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} data
|
||||
* @param {*} index
|
||||
* @returns
|
||||
*/
|
||||
_getItemLayout(data, index) {
|
||||
const { _columns, _thumbnailHeight } = this.props;
|
||||
const cellHeight = _thumbnailHeight;
|
||||
|
||||
return {
|
||||
length: cellHeight,
|
||||
offset: cellHeight * Math.floor(index / _columns),
|
||||
index
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler for visible items changes.
|
||||
*
|
||||
* @param {Object} data - The visible items data.
|
||||
* @param {Array<Object>} data.viewableItems - The visible items array.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onViewableItemsChanged({ viewableItems = 0 }) {
|
||||
console.log(`visisble indexes ${JSON.stringify((viewableItems || []).map(i => i.index))}`);
|
||||
const indexArray = viewableItems.map(i => i.index);
|
||||
|
||||
this.props.dispatch(setVisibleRemoteParticipants(Math.min(indexArray), Math.max(indexArray)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,54 +139,36 @@ class TileView extends Component<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _height, _width, onClick } = this.props;
|
||||
const rowElements = this._groupIntoRows(this._renderThumbnails(), this._getColumnCount());
|
||||
const { _columns, _height, _thumbnailHeight, _width, onClick } = this.props;
|
||||
const participants = this._getSortedParticipants();
|
||||
const initialRowsToRender = Math.ceil(_height / _thumbnailHeight);
|
||||
|
||||
if (this._flatListStyles.height !== _height || this._flatListStyles.width !== _width) {
|
||||
this._flatListStyles = {
|
||||
height: _height,
|
||||
width: _width
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style = {{
|
||||
...styles.tileView,
|
||||
height: _height,
|
||||
width: _width
|
||||
}}>
|
||||
<TouchableWithoutFeedback onPress = { onClick }>
|
||||
<View
|
||||
style = {{
|
||||
...styles.tileViewRows,
|
||||
minHeight: _height,
|
||||
minWidth: _width
|
||||
}}>
|
||||
{ rowElements }
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</ScrollView>
|
||||
<TouchableWithoutFeedback onPress = { onClick }>
|
||||
<FlatList
|
||||
contentContainerStyle = { styles.tileView }
|
||||
data = { participants }
|
||||
horizontal = { false }
|
||||
initialNumToRender = { initialRowsToRender }
|
||||
key = { _columns }
|
||||
keyExtractor = { this._keyExtractor }
|
||||
numColumns = { _columns }
|
||||
onViewableItemsChanged = { this._onViewableItemsChanged }
|
||||
renderItem = { this._renderThumbnail }
|
||||
style = { this._flatListStyles }
|
||||
viewabilityConfig = { this.viewabilityConfig }
|
||||
windowSize = { 2 } />
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many columns should be displayed for tile view.
|
||||
*
|
||||
* @returns {number}
|
||||
* @private
|
||||
*/
|
||||
_getColumnCount() {
|
||||
const participantCount = this.props._participantCount;
|
||||
|
||||
// For narrow view, tiles should stack on top of each other for a lonely
|
||||
// call and a 1:1 call. Otherwise tiles should be grouped into rows of
|
||||
// two.
|
||||
if (this.props._aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
return participantCount >= 3 ? 2 : 1;
|
||||
}
|
||||
|
||||
if (participantCount === 4) {
|
||||
// In wide view, a four person call should display as a 2x2 grid.
|
||||
return 2;
|
||||
}
|
||||
|
||||
return Math.min(3, participantCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all participants with the local participant at the end.
|
||||
*
|
||||
@@ -175,63 +184,6 @@ class TileView extends Component<Props> {
|
||||
return participants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the height and width for the tiles.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getTileDimensions() {
|
||||
const { _height, _participantCount, _width } = this.props;
|
||||
const columns = this._getColumnCount();
|
||||
const heightToUse = _height - (MARGIN * 2);
|
||||
const widthToUse = _width - (MARGIN * 2);
|
||||
let tileWidth;
|
||||
|
||||
// If there is going to be at least two rows, ensure that at least two
|
||||
// rows display fully on screen.
|
||||
if (_participantCount / columns > 1) {
|
||||
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
|
||||
} else {
|
||||
tileWidth = Math.min(widthToUse / columns, heightToUse);
|
||||
}
|
||||
|
||||
return {
|
||||
height: tileWidth / TILE_ASPECT_RATIO,
|
||||
width: tileWidth
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a list of thumbnails into React Elements with a maximum of
|
||||
* {@link rowLength} thumbnails in each.
|
||||
*
|
||||
* @param {Array} thumbnails - The list of thumbnails that should be split
|
||||
* into separate row groupings.
|
||||
* @param {number} rowLength - How many thumbnails should be in each row.
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
*/
|
||||
_groupIntoRows(thumbnails, rowLength) {
|
||||
const rowElements = [];
|
||||
|
||||
for (let i = 0; i < thumbnails.length; i++) {
|
||||
if (i % rowLength === 0) {
|
||||
const thumbnailsInRow = thumbnails.slice(i, i + rowLength);
|
||||
|
||||
rowElements.push(
|
||||
<View
|
||||
key = { rowElements.length }
|
||||
style = { styles.tileViewRow }>
|
||||
{ thumbnailsInRow }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return rowElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates React Elements to display each participant in a thumbnail. Each
|
||||
* tile will be.
|
||||
@@ -239,43 +191,29 @@ class TileView extends Component<Props> {
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
*/
|
||||
_renderThumbnails() {
|
||||
_renderThumbnail({ item, index /*, separators */ }) {
|
||||
const { _thumbnailHeight } = this.props;
|
||||
const styleOverrides = {
|
||||
aspectRatio: TILE_ASPECT_RATIO,
|
||||
aspectRatio: SQUARE_TILE_ASPECT_RATIO,
|
||||
flex: 0,
|
||||
height: this._getTileDimensions().height,
|
||||
height: _thumbnailHeight,
|
||||
maxHeight: null,
|
||||
maxWidth: null,
|
||||
width: null
|
||||
};
|
||||
|
||||
return this._getSortedParticipants()
|
||||
.map(id => (
|
||||
<Thumbnail
|
||||
disableTint = { true }
|
||||
key = { id }
|
||||
participantID = { id }
|
||||
renderDisplayName = { true }
|
||||
styleOverrides = { styleOverrides }
|
||||
tileView = { true } />));
|
||||
}
|
||||
console.log(`1rendering thumbnail with index ${index}`);
|
||||
|
||||
/**
|
||||
* Sets the receiver video quality based on the dimensions of the thumbnails
|
||||
* that are displayed.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateReceiverQuality() {
|
||||
const { height, width } = this._getTileDimensions();
|
||||
|
||||
this.props.dispatch(setTileViewDimensions({
|
||||
thumbnailSize: {
|
||||
height,
|
||||
width
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<Thumbnail
|
||||
disableTint = { true }
|
||||
height = { _thumbnailHeight }
|
||||
index = { index }
|
||||
key = { item }
|
||||
participantID = { item }
|
||||
renderDisplayName = { true }
|
||||
tileView = { true } />)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,14 +226,18 @@ class TileView extends Component<Props> {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const responsiveUi = state['features/base/responsive-ui'];
|
||||
const { remoteParticipants } = state['features/filmstrip'];
|
||||
const { remoteParticipants, tileViewDimensions } = state['features/filmstrip'];
|
||||
const { height } = tileViewDimensions.thumbnailSize;
|
||||
const { columns } = tileViewDimensions;
|
||||
|
||||
return {
|
||||
_aspectRatio: responsiveUi.aspectRatio,
|
||||
_columns: columns,
|
||||
_height: responsiveUi.clientHeight,
|
||||
_localParticipant: getLocalParticipant(state),
|
||||
_participantCount: getParticipantCountWithFake(state),
|
||||
_remoteParticipants: remoteParticipants,
|
||||
_thumbnailHeight: height,
|
||||
_width: responsiveUi.clientWidth
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,16 +125,10 @@ export default {
|
||||
},
|
||||
|
||||
tileView: {
|
||||
alignSelf: 'center'
|
||||
},
|
||||
|
||||
tileViewRows: {
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
tileViewRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
justifyContent: 'center'
|
||||
// flexGrow: 1
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { getToolbarButtons } from '../../../base/config';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
@@ -17,7 +18,13 @@ import { showToolbox } from '../../../toolbox/actions.web';
|
||||
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||
import { setFilmstripVisible, setVisibleRemoteParticipants } from '../../actions';
|
||||
import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, TOOLBAR_HEIGHT } from '../../constants';
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
TILE_HORIZONTAL_MARGIN,
|
||||
TILE_VERTICAL_MARGIN,
|
||||
TOOLBAR_HEIGHT,
|
||||
TOOLBAR_HEIGHT_MOBILE
|
||||
} from '../../constants';
|
||||
import { shouldRemoteVideosBeVisible } from '../../functions';
|
||||
|
||||
import AudioTracksContainer from './AudioTracksContainer';
|
||||
@@ -485,10 +492,6 @@ function _mapStateToProps(state) {
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
|
||||
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
|
||||
const { isOpen: shiftRight } = state['features/chat'];
|
||||
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
|
||||
reduceHeight ? 'reduce-height' : ''
|
||||
} ${shiftRight ? 'shift-right' : ''}`.trim();
|
||||
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
|
||||
const {
|
||||
gridDimensions = {},
|
||||
filmstripHeight,
|
||||
@@ -496,12 +499,35 @@ function _mapStateToProps(state) {
|
||||
thumbnailSize: tileViewThumbnailSize
|
||||
} = state['features/filmstrip'].tileViewDimensions;
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const availableSpace = clientHeight - filmstripHeight;
|
||||
let filmstripPadding = 0;
|
||||
|
||||
if (availableSpace > 0) {
|
||||
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
|
||||
|
||||
if (paddingValue > 0) {
|
||||
filmstripPadding = paddingValue;
|
||||
}
|
||||
} else {
|
||||
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
|
||||
}
|
||||
|
||||
const collapseTileView = reduceHeight
|
||||
&& isMobileBrowser()
|
||||
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
|
||||
|
||||
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
|
||||
reduceHeight ? 'reduce-height' : ''
|
||||
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''}`.trim();
|
||||
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
|
||||
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
_thumbnailSize = tileViewThumbnailSize;
|
||||
remoteFilmstripHeight = filmstripHeight;
|
||||
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
|
||||
remoteFilmstripWidth = filmstripWidth;
|
||||
break;
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isMobileBrowser } from '../../../../../react/features/base/environment/utils';
|
||||
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
|
||||
import { AudioLevelIndicator } from '../../../audio-level-indicator';
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
|
||||
import {
|
||||
@@ -139,6 +139,11 @@ export type Props = {|
|
||||
*/
|
||||
_isCurrentlyOnLargeVideo: boolean,
|
||||
|
||||
/**
|
||||
* Whether we are currently running in a mobile browser.
|
||||
*/
|
||||
_isMobile: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the participant is screen sharing.
|
||||
*/
|
||||
@@ -612,7 +617,7 @@ class Thumbnail extends Component<Props, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFakeParticipant() {
|
||||
const { _participant: { avatarURL } } = this.props;
|
||||
const { _isMobile, _participant: { avatarURL } } = this.props;
|
||||
const styles = this._getStyles();
|
||||
const containerClassName = this._getContainerClassName();
|
||||
|
||||
@@ -621,8 +626,10 @@ class Thumbnail extends Component<Props, State> {
|
||||
className = { containerClassName }
|
||||
id = 'sharedVideoContainer'
|
||||
onClick = { this._onClick }
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
{ ...(_isMobile ? {} : {
|
||||
onMouseEnter: this._onMouseEnter,
|
||||
onMouseLeave: this._onMouseLeave
|
||||
}) }
|
||||
style = { styles.thumbnail }>
|
||||
{avatarURL ? (
|
||||
<img
|
||||
@@ -753,6 +760,7 @@ class Thumbnail extends Component<Props, State> {
|
||||
const {
|
||||
_defaultLocalDisplayName,
|
||||
_disableLocalVideoFlip,
|
||||
_isMobile,
|
||||
_isScreenSharing,
|
||||
_localFlipX,
|
||||
_disableProfile,
|
||||
@@ -772,13 +780,17 @@ class Thumbnail extends Component<Props, State> {
|
||||
className = { containerClassName }
|
||||
id = 'localVideoContainer'
|
||||
onClick = { this._onClick }
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
{ ...(isMobileBrowser() ? {
|
||||
onTouchEnd: this._onTouchEnd,
|
||||
onTouchMove: this._onTouchMove,
|
||||
onTouchStart: this._onTouchStart
|
||||
} : {}) }
|
||||
{ ...(_isMobile
|
||||
? {
|
||||
onTouchEnd: this._onTouchEnd,
|
||||
onTouchMove: this._onTouchMove,
|
||||
onTouchStart: this._onTouchStart
|
||||
}
|
||||
: {
|
||||
onMouseEnter: this._onMouseEnter,
|
||||
onMouseLeave: this._onMouseLeave
|
||||
}
|
||||
) }
|
||||
style = { styles.thumbnail }>
|
||||
<div className = 'videocontainer__background' />
|
||||
<span id = 'localVideoWrapper'>
|
||||
@@ -875,6 +887,7 @@ class Thumbnail extends Component<Props, State> {
|
||||
*/
|
||||
_renderRemoteParticipant() {
|
||||
const {
|
||||
_isMobile,
|
||||
_isTestModeEnabled,
|
||||
_participant,
|
||||
_startSilent,
|
||||
@@ -909,13 +922,17 @@ class Thumbnail extends Component<Props, State> {
|
||||
className = { containerClassName }
|
||||
id = { `participant_${id}` }
|
||||
onClick = { this._onClick }
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
{ ...(isMobileBrowser() ? {
|
||||
onTouchEnd: this._onTouchEnd,
|
||||
onTouchMove: this._onTouchMove,
|
||||
onTouchStart: this._onTouchStart
|
||||
} : {}) }
|
||||
{ ...(_isMobile
|
||||
? {
|
||||
onTouchEnd: this._onTouchEnd,
|
||||
onTouchMove: this._onTouchMove,
|
||||
onTouchStart: this._onTouchStart
|
||||
}
|
||||
: {
|
||||
onMouseEnter: this._onMouseEnter,
|
||||
onMouseLeave: this._onMouseLeave
|
||||
}
|
||||
) }
|
||||
style = { styles.thumbnail }>
|
||||
{
|
||||
_videoTrack && <VideoTrack
|
||||
@@ -1031,6 +1048,7 @@ function _mapStateToProps(state, ownProps): Object {
|
||||
} = state['features/base/config'];
|
||||
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
const _isMobile = isMobileBrowser();
|
||||
|
||||
|
||||
switch (_currentLayout) {
|
||||
@@ -1072,7 +1090,7 @@ function _mapStateToProps(state, ownProps): Object {
|
||||
return {
|
||||
_audioTrack,
|
||||
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
|
||||
_connectionIndicatorDisabled: isMobileBrowser() || interfaceConfig.CONNECTION_INDICATOR_DISABLED,
|
||||
_connectionIndicatorDisabled: _isMobile || interfaceConfig.CONNECTION_INDICATOR_DISABLED,
|
||||
_currentLayout,
|
||||
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
|
||||
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
|
||||
@@ -1081,6 +1099,7 @@ function _mapStateToProps(state, ownProps): Object {
|
||||
_isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
|
||||
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
|
||||
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
|
||||
_isMobile,
|
||||
_isScreenSharing: _videoTrack?.videoType === 'desktop',
|
||||
_isTestModeEnabled: isTestModeEnabled(state),
|
||||
_isVideoPlayable: id && isVideoPlayable(state, id),
|
||||
|
||||
@@ -163,6 +163,11 @@ export const TILE_HORIZONTAL_MARGIN = 4;
|
||||
*/
|
||||
export const TOOLBAR_HEIGHT = 72;
|
||||
|
||||
/**
|
||||
* The height of the whole toolbar.
|
||||
*/
|
||||
export const TOOLBAR_HEIGHT_MOBILE = 60;
|
||||
|
||||
/**
|
||||
* The size of the horizontal border of a thumbnail.
|
||||
*
|
||||
@@ -215,3 +220,14 @@ export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
|
||||
* @type {number}
|
||||
*/
|
||||
export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
|
||||
|
||||
/**
|
||||
* The margin for each side of the tile view. Taken away from the available
|
||||
* height and width for the tile container to display in.
|
||||
*
|
||||
* NOTE: Mobile specific.
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
export const TILE_MARGIN = 10;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
import { ASPECT_RATIO_NARROW } from '../base/responsive-ui/constants';
|
||||
|
||||
/**
|
||||
* Returns true if the filmstrip on mobile is visible, false otherwise.
|
||||
@@ -25,3 +26,31 @@ export function isFilmstripVisible(stateful: Object | Function) {
|
||||
|
||||
return getParticipantCountWithFake(state) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many columns should be displayed for tile view.
|
||||
*
|
||||
* @param {Object | Function} stateful - The Object or Function that can be
|
||||
* resolved to a Redux state object with the toState function.
|
||||
* @returns {number} - The number of columns to be rendered in tile view.
|
||||
* @private
|
||||
*/
|
||||
export function getColumnCount(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
const { aspectRatio } = state['features/base/responsive-ui'];
|
||||
|
||||
// For narrow view, tiles should stack on top of each other for a lonely
|
||||
// call and a 1:1 call. Otherwise tiles should be grouped into rows of
|
||||
// two.
|
||||
if (aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
return participantCount >= 3 ? 2 : 1;
|
||||
}
|
||||
|
||||
if (participantCount === 4) {
|
||||
// In wide view, a four person call should display as a 2x2 grid.
|
||||
return 2;
|
||||
}
|
||||
|
||||
return Math.min(3, participantCount);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { CLIENT_RESIZED, SET_ASPECT_RATIO } from '../base/responsive-ui/actionTypes';
|
||||
|
||||
import { setTileViewDimensions } from './actions';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Middleware that handles widnow dimension changes and updates the aspect ratio.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(({ dispatch }) => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case CLIENT_RESIZED:
|
||||
case SET_ASPECT_RATIO:
|
||||
dispatch(setTileViewDimensions());
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ const DEFAULT_STATE = {
|
||||
horizontalViewDimensions: {},
|
||||
|
||||
/**
|
||||
* The custom audio volume levels per perticipant.
|
||||
* The custom audio volume levels per participant.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
@@ -158,7 +158,9 @@ ReducerRegistry.register(
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
return {
|
||||
...state
|
||||
};
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
const { id, local } = action.participant;
|
||||
@@ -187,7 +189,9 @@ ReducerRegistry.register(
|
||||
|
||||
delete state.participantsVolume[id];
|
||||
|
||||
return state;
|
||||
return {
|
||||
...state
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
41
react/features/filmstrip/subscriber.native.js
Normal file
41
react/features/filmstrip/subscriber.native.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
import { getTileViewGridDimensions, shouldDisplayTileView } from '../video-layout';
|
||||
|
||||
import { setTileViewDimensions } from './actions';
|
||||
|
||||
/**
|
||||
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => {
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
|
||||
if (participantCount < 5) { // the dimensions are updated only when the participant count is lower than 5.
|
||||
return participantCount;
|
||||
}
|
||||
|
||||
return 4; // make sure we don't update the dimensions.
|
||||
},
|
||||
/* listener */ (_, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (shouldDisplayTileView(state)) {
|
||||
store.dispatch(setTileViewDimensions());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Listens for changes in the selected layout to calculate the dimensions of the tile view grid and horizontal view.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => shouldDisplayTileView(state),
|
||||
/* listener */ (isTileView, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (isTileView) {
|
||||
store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
|
||||
}
|
||||
});
|
||||
@@ -8,14 +8,15 @@ import { Dialog } from '../../../../base/dialog';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import { isVpaasMeeting } from '../../../../billing-counter/functions';
|
||||
import { isDynamicBrandingDataLoaded } from '../../../../dynamic-branding/functions';
|
||||
import EmbedMeetingTrigger from '../../../../embed-meeting/components/EmbedMeetingTrigger';
|
||||
import { isVpaasMeeting } from '../../../../jaas/functions';
|
||||
import { getActiveSession } from '../../../../recording';
|
||||
import { updateDialInNumbers } from '../../../actions';
|
||||
import {
|
||||
_getDefaultPhoneNumber,
|
||||
getInviteText,
|
||||
getInviteTextiOS,
|
||||
isAddPeopleEnabled,
|
||||
isDialOutEnabled,
|
||||
sharingFeatures,
|
||||
@@ -62,6 +63,12 @@ type Props = {
|
||||
*/
|
||||
_invitationText: string,
|
||||
|
||||
/**
|
||||
* The custom no new-lines meeting invitation text for iOS default email.
|
||||
* Needed because of this mailto: iOS issue: https://developer.apple.com/forums/thread/681023
|
||||
*/
|
||||
_invitationTextiOS: string,
|
||||
|
||||
/**
|
||||
* An alternate app name to be displayed in the email subject.
|
||||
*/
|
||||
@@ -110,6 +117,7 @@ function AddPeopleDialog({
|
||||
_urlSharingVisible,
|
||||
_emailSharingVisible,
|
||||
_invitationText,
|
||||
_invitationTextiOS,
|
||||
_inviteAppName,
|
||||
_inviteContactsVisible,
|
||||
_inviteUrl,
|
||||
@@ -160,7 +168,8 @@ function AddPeopleDialog({
|
||||
_emailSharingVisible
|
||||
? <InviteByEmailSection
|
||||
inviteSubject = { inviteSubject }
|
||||
inviteText = { _invitationText } />
|
||||
inviteText = { _invitationText }
|
||||
inviteTextiOS = { _invitationTextiOS } />
|
||||
: null
|
||||
}
|
||||
{ _embedMeetingVisible && <EmbedMeetingTrigger /> }
|
||||
@@ -207,6 +216,9 @@ function mapStateToProps(state, ownProps) {
|
||||
_invitationText: getInviteText({ state,
|
||||
phoneNumber,
|
||||
t: ownProps.t }),
|
||||
_invitationTextiOS: getInviteTextiOS({ state,
|
||||
phoneNumber,
|
||||
t: ownProps.t }),
|
||||
_inviteAppName: inviteAppName,
|
||||
_inviteContactsVisible: interfaceConfig.ENABLE_DIAL_OUT && !hideInviteContacts,
|
||||
_inviteUrl: getInviteURL(state),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { isIosMobileBrowser } from '../../../../base/environment/utils';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import {
|
||||
Icon,
|
||||
@@ -27,6 +28,11 @@ type Props = {
|
||||
*/
|
||||
inviteText: string,
|
||||
|
||||
/**
|
||||
* The encoded no new-lines iOS invitation text to be sent on default mail.
|
||||
*/
|
||||
inviteTextiOS: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
@@ -38,10 +44,13 @@ type Props = {
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
|
||||
function InviteByEmailSection({ inviteSubject, inviteText, inviteTextiOS, t }: Props) {
|
||||
const [ isActive, setIsActive ] = useState(false);
|
||||
const encodedInviteSubject = encodeURIComponent(inviteSubject);
|
||||
const encodedInviteText = encodeURIComponent(inviteText);
|
||||
const encodedInviteTextiOS = encodeURIComponent(inviteTextiOS);
|
||||
|
||||
const encodedDefaultEmailText = isIosMobileBrowser() ? encodedInviteTextiOS : encodedInviteText;
|
||||
|
||||
/**
|
||||
* Copies the conference invitation to the clipboard.
|
||||
@@ -100,7 +109,7 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
|
||||
{
|
||||
icon: IconEmail,
|
||||
tooltipKey: 'addPeople.defaultEmail',
|
||||
url: `mailto:?subject=${encodedInviteSubject}&body=${encodedInviteText}`
|
||||
url: `mailto:?subject=${encodedInviteSubject}&body=${encodedDefaultEmailText}`
|
||||
},
|
||||
{
|
||||
icon: IconGoogle,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Icon, IconPhone } from '../../../../base/icons';
|
||||
import { getLocalParticipant } from '../../../../base/participants';
|
||||
import { MultiSelectAutocomplete } from '../../../../base/react';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import { isVpaasMeeting } from '../../../../billing-counter/functions';
|
||||
import { isVpaasMeeting } from '../../../../jaas/functions';
|
||||
import { hideAddPeopleDialog } from '../../../actions';
|
||||
import { INVITE_TYPES } from '../../../constants';
|
||||
import AbstractAddPeopleDialog, {
|
||||
@@ -511,11 +511,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
const supportString = t('inlineDialogFailure.supportMsg');
|
||||
const supportLink = interfaceConfig.SUPPORT_URL;
|
||||
|
||||
if (!supportLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const supportLinkContent = (
|
||||
const supportLinkContent = supportLink ? (
|
||||
<span>
|
||||
<span>
|
||||
{ supportString.padEnd(supportString.length + 1) }
|
||||
@@ -524,13 +520,14 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
<a
|
||||
aria-label = { supportLink }
|
||||
href = { supportLink }
|
||||
rel = 'noopener noreferrer'>
|
||||
rel = 'noopener noreferrer'
|
||||
target = '_blank'>
|
||||
{ t('inlineDialogFailure.support') }
|
||||
</a>
|
||||
</span>
|
||||
<span>.</span>
|
||||
</span>
|
||||
);
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className = 'modal-dialog-form-error'>
|
||||
@@ -595,7 +592,7 @@ function _mapStateToProps(state) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
_footerTextEnabled: footerTextEnabled,
|
||||
_isVpaas: isVpaasMeeting(state, false)
|
||||
_isVpaas: isVpaasMeeting(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class DialInSummary extends Component<Props> {
|
||||
headerLabelKey: 'info.label'
|
||||
}}
|
||||
modalId = { DIAL_IN_SUMMARY_VIEW_ID }
|
||||
style = { styles.backDrop } >
|
||||
style = { styles.backDrop }>
|
||||
<WebView
|
||||
onError = { this._onError }
|
||||
onShouldStartLoadWithRequest = { this._onNavigate }
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import { getActiveSession } from '../../features/recording/functions';
|
||||
import { getRoomName } from '../base/conference';
|
||||
import { getInviteURL } from '../base/connection';
|
||||
import { isIosMobileBrowser } from '../base/environment/utils';
|
||||
import { i18next } from '../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
||||
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
import { doGetJSON, parseURIString } from '../base/util';
|
||||
import { isVpaasMeeting } from '../billing-counter/functions';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
|
||||
import { INVITE_TYPES, SIP_ADDRESS_REGEX } from './constants';
|
||||
import logger from './logger';
|
||||
@@ -251,6 +252,49 @@ export function getInviteResultsForQuery(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a custom no new lines message for iOS default mail describing how to dial in to the conference.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getInviteTextiOS({
|
||||
state,
|
||||
phoneNumber,
|
||||
t
|
||||
}: Object) {
|
||||
if (!isIosMobileBrowser()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dialIn = state['features/invite'];
|
||||
const inviteUrl = getInviteURL(state);
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const localParticipantName = localParticipant?.name;
|
||||
|
||||
const inviteURL = _decodeRoomURI(inviteUrl);
|
||||
|
||||
let invite = localParticipantName
|
||||
? t('info.inviteTextiOSPersonal', { name: localParticipantName })
|
||||
: t('info.inviteURLFirstPartGeneral');
|
||||
|
||||
invite += ' ';
|
||||
|
||||
invite += t('info.inviteTextiOSInviteUrl', { inviteUrl });
|
||||
invite += ' ';
|
||||
|
||||
if (shouldDisplayDialIn(dialIn)) {
|
||||
invite += t('info.inviteTextiOSPhone', {
|
||||
number: phoneNumber,
|
||||
conferenceID: dialIn.conferenceID,
|
||||
didUrl: getDialInfoPageURL(state)
|
||||
});
|
||||
}
|
||||
invite += ' ';
|
||||
invite += t('info.inviteTextiOSJoinSilent', { silentUrl: `${inviteURL}#config.startSilent=true` });
|
||||
|
||||
return invite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message describing how to dial in to the conference.
|
||||
*
|
||||
@@ -271,7 +315,6 @@ export function getInviteText({
|
||||
const localParticipantName = localParticipant?.name;
|
||||
|
||||
const inviteURL = _decodeRoomURI(inviteUrl);
|
||||
|
||||
let invite = localParticipantName
|
||||
? t('info.inviteURLFirstPartPersonal', { name: localParticipantName })
|
||||
: t('info.inviteURLFirstPartGeneral');
|
||||
@@ -396,7 +439,7 @@ export function isDialOutEnabled(state: Object): boolean {
|
||||
*/
|
||||
export function isSipInviteEnabled(state: Object): boolean {
|
||||
const { sipInviteUrl } = state['features/base/config'];
|
||||
const { features = {} } = getLocalParticipant(state);
|
||||
const { features = {} } = getLocalParticipant(state) || {};
|
||||
|
||||
return state['features/base/jwt'].jwt
|
||||
&& Boolean(sipInviteUrl)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import { openDialog } from '../base/dialog';
|
||||
import { VPAAS_TENANT_PREFIX } from '../billing-counter/constants';
|
||||
import { getVpaasTenant } from '../billing-counter/functions';
|
||||
|
||||
import { SET_DETAILS } from './actionTypes';
|
||||
import { PremiumFeatureDialog } from './components';
|
||||
import { isFeatureDisabled, sendGetDetailsRequest } from './functions';
|
||||
import { VPAAS_TENANT_PREFIX } from './constants';
|
||||
import { getVpaasTenant, isFeatureDisabled, sendGetDetailsRequest } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,3 +23,8 @@ export const FEATURES = {
|
||||
* URL for displaying JaaS upgrade options
|
||||
*/
|
||||
export const JAAS_UPGRADE_URL = 'https://jaas.8x8.vc/#/plan/upgrade';
|
||||
|
||||
/**
|
||||
* The prefix for the vpaas tenant.
|
||||
*/
|
||||
export const VPAAS_TENANT_PREFIX = 'vpaas-magic-cookie-';
|
||||
|
||||
@@ -1,9 +1,53 @@
|
||||
// @flow
|
||||
|
||||
import { getVpaasTenant } from '../billing-counter/functions';
|
||||
|
||||
import { VPAAS_TENANT_PREFIX } from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Returns the full vpaas tenant if available, given a path.
|
||||
*
|
||||
* @param {string} path - The meeting url path.
|
||||
* @returns {string}
|
||||
*/
|
||||
function extractVpaasTenantFromPath(path: string) {
|
||||
const [ , tenant ] = path.split('/');
|
||||
|
||||
if (tenant.startsWith(VPAAS_TENANT_PREFIX)) {
|
||||
return tenant;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the vpaas tenant.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getVpaasTenant(state: Object) {
|
||||
return extractVpaasTenantFromPath(state['features/base/connection'].locationURL.pathname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current meeting is a vpaas one.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVpaasMeeting(state: Object) {
|
||||
const connection = state['features/base/connection'];
|
||||
|
||||
if (connection?.locationURL?.pathname) {
|
||||
return Boolean(
|
||||
extractVpaasTenantFromPath(connection?.locationURL?.pathname)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request for retrieving jaas customer details.
|
||||
*
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { sendAnalytics, createVpaasConferenceJoinedEvent } from '../analytics';
|
||||
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
||||
import { PARTICIPANT_JOINED } from '../base/participants/actionTypes';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import { countEndpoint } from './actions';
|
||||
import { isVpaasMeeting, extractVpaasTenantFromPath } from './functions';
|
||||
import { isVpaasMeeting, getVpaasTenant } from './functions';
|
||||
|
||||
/**
|
||||
* The redux middleware for billing counter.
|
||||
@@ -20,17 +18,6 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_JOINED: {
|
||||
const shouldCount = !store.getState()['features/billing-counter'].endpointCounted
|
||||
&& !action.participant.local;
|
||||
|
||||
if (shouldCount) {
|
||||
store.dispatch(countEndpoint());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
@@ -45,7 +32,6 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
function _maybeTrackVpaasConferenceJoin(state) {
|
||||
if (isVpaasMeeting(state)) {
|
||||
sendAnalytics(createVpaasConferenceJoinedEvent(
|
||||
extractVpaasTenantFromPath(
|
||||
state['features/base/connection'].locationURL.pathname)));
|
||||
getVpaasTenant(state)));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
import { redirectToStaticPage } from '../app/actions';
|
||||
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
||||
import { CONNECTION_FAILED } from '../base/connection';
|
||||
import { JitsiConnectionErrors } from '../base/lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
|
||||
import { SET_DETAILS } from './actionTypes';
|
||||
import { getCustomerDetails } from './actions';
|
||||
import { STATUSES } from './constants';
|
||||
import { isVpaasMeeting } from './functions';
|
||||
|
||||
/**
|
||||
* The redux middleware for billing counter.
|
||||
* The redux middleware for jaas.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
|
||||
MiddlewareRegistry.register(store => next => async action => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
@@ -20,6 +23,22 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case CONNECTION_FAILED: {
|
||||
const { error } = action;
|
||||
|
||||
if (!isVpaasMeeting(store.getState()) || !error) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (error.name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
|
||||
if (error.message !== 'could not obtain public key') {
|
||||
break;
|
||||
}
|
||||
|
||||
store.dispatch(redirectToStaticPage('/static/planLimit.html'));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SET_DETAILS: {
|
||||
const { status } = action.payload;
|
||||
|
||||
|
||||
15
react/features/large-video/functions.js
Normal file
15
react/features/large-video/functions.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import { getParticipantById } from '../base/participants';
|
||||
|
||||
/**
|
||||
* Selector for the participant currently displaying on the large video.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getLargeVideoParticipant(state: Object) {
|
||||
const { participantId } = state['features/large-video'];
|
||||
|
||||
return getParticipantById(state, participantId);
|
||||
}
|
||||
@@ -1,28 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
|
||||
/**
|
||||
* Approves (lets in) or rejects a knocking participant.
|
||||
*
|
||||
* @param {Function} getState - Function to get the Redux state.
|
||||
* @param {string} id - The id of the knocking participant.
|
||||
* @param {boolean} approved - True if the participant is approved, false otherwise.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setKnockingParticipantApproval(getState: Function, id: string, approved: boolean) {
|
||||
const conference = getCurrentConference(getState());
|
||||
|
||||
if (conference) {
|
||||
if (approved) {
|
||||
conference.lobbyApproveAccess(id);
|
||||
} else {
|
||||
conference.lobbyDenyAccess(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Selector to return lobby state.
|
||||
*
|
||||
@@ -32,3 +9,16 @@ export function setKnockingParticipantApproval(getState: Function, id: string, a
|
||||
export function getLobbyState(state: any) {
|
||||
return state['features/lobby'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Selector to return array with knocking participant ids.
|
||||
*
|
||||
* @param {any} state - State object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function getKnockingParticipantsById(state: any) {
|
||||
const { knockingParticipants } = state['features/lobby'];
|
||||
|
||||
return knockingParticipants.map(participant => participant.id);
|
||||
}
|
||||
|
||||
@@ -1,352 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
import { RTCPeerConnection, RTCSessionDescription } from 'react-native-webrtc';
|
||||
import { RTCPeerConnection as PC } from 'react-native-webrtc';
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
// Address families.
|
||||
const AF_INET6 = 30; /* IPv6 */
|
||||
|
||||
// Protocols (RFC 1700)
|
||||
const IPPROTO_TCP = 6; /* tcp */
|
||||
const IPPROTO_UDP = 17; /* user datagram protocol */
|
||||
|
||||
// Protocol families, same as address families for now.
|
||||
const PF_INET6 = AF_INET6;
|
||||
|
||||
const SOCK_DGRAM = 2; /* datagram socket */
|
||||
const SOCK_STREAM = 1; /* stream socket */
|
||||
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
// XXX At the time of this writing extending RTCPeerConnection using ES6 'class'
|
||||
// and 'extends' causes a runtime error related to the attempt to define the
|
||||
// onaddstream property setter. The error mentions that babelHelpers.set is
|
||||
// undefined which appears to be a thing inside React Native's packager. As a
|
||||
// workaround, extend using the pre-ES6 way.
|
||||
import { synthesizeIPv6Addresses } from './ipv6utils';
|
||||
|
||||
/**
|
||||
* The RTCPeerConnection provided by react-native-webrtc fires onaddstream
|
||||
* before it remembers remotedescription (and thus makes it available to API
|
||||
* clients). Because that appears to be a problem for lib-jitsi-meet which has
|
||||
* been successfully running on Chrome, Firefox and others for a very long
|
||||
* time, attempt to meet its expectations (by extending RTCPPeerConnection).
|
||||
*
|
||||
* @class
|
||||
* Override PeerConnection to synthesize IPv6 addresses.
|
||||
*/
|
||||
export default function _RTCPeerConnection(...args: any[]) {
|
||||
export default class RTCPeerConnection extends PC {
|
||||
|
||||
/* eslint-disable indent, no-invalid-this */
|
||||
|
||||
RTCPeerConnection.apply(this, args);
|
||||
|
||||
this.onaddstream = (...args) => // eslint-disable-line no-shadow
|
||||
(this._onaddstreamQueue
|
||||
? this._queueOnaddstream
|
||||
: this._invokeOnaddstream)
|
||||
.apply(this, args);
|
||||
|
||||
// Shadow RTCPeerConnection's onaddstream but after _RTCPeerConnection has
|
||||
// assigned to the property in question. Defining the property on
|
||||
// _RTCPeerConnection's prototype may (or may not, I don't know) work but I
|
||||
// don't want to try because the following approach appears to work and I
|
||||
// understand it.
|
||||
|
||||
// $FlowFixMe
|
||||
Object.defineProperty(this, 'onaddstream', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return this._onaddstream;
|
||||
},
|
||||
set(value) {
|
||||
this._onaddstream = value;
|
||||
}
|
||||
});
|
||||
|
||||
/* eslint-enable indent, no-invalid-this */
|
||||
}
|
||||
|
||||
_RTCPeerConnection.prototype = Object.create(RTCPeerConnection.prototype);
|
||||
_RTCPeerConnection.prototype.constructor = _RTCPeerConnection;
|
||||
|
||||
_RTCPeerConnection.prototype._invokeOnaddstream = function(...args) {
|
||||
const onaddstream = this._onaddstream;
|
||||
|
||||
return onaddstream && onaddstream.apply(this, args);
|
||||
};
|
||||
|
||||
_RTCPeerConnection.prototype._invokeQueuedOnaddstream = function(q) {
|
||||
q && q.forEach(args => {
|
||||
try {
|
||||
this._invokeOnaddstream(...args);
|
||||
} catch (e) {
|
||||
// TODO Determine whether the combination of the standard
|
||||
// setRemoteDescription and onaddstream results in a similar
|
||||
// swallowing of errors.
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_RTCPeerConnection.prototype._queueOnaddstream = function(...args) {
|
||||
this._onaddstreamQueue.push(Array.from(args));
|
||||
};
|
||||
|
||||
_RTCPeerConnection.prototype.setRemoteDescription = function(description) {
|
||||
|
||||
return (
|
||||
_synthesizeIPv6Addresses(description)
|
||||
.catch(reason => {
|
||||
reason && console.error(reason);
|
||||
|
||||
return description;
|
||||
})
|
||||
.then(value => _setRemoteDescription.bind(this)(value)));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Adapts react-native-webrtc's {@link RTCPeerConnection#setRemoteDescription}
|
||||
* implementation which uses the deprecated, callback-based version to the
|
||||
* {@code Promise}-based version.
|
||||
*
|
||||
* @param {RTCSessionDescription} description - The RTCSessionDescription
|
||||
* which specifies the configuration of the remote end of the connection.
|
||||
* @private
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _setRemoteDescription(description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
/* eslint-disable no-invalid-this */
|
||||
|
||||
// Ensure I'm not remembering onaddstream invocations from previous
|
||||
// setRemoteDescription calls. I shouldn't be but... anyway.
|
||||
this._onaddstreamQueue = [];
|
||||
|
||||
RTCPeerConnection.prototype.setRemoteDescription.call(this, description)
|
||||
.then((...args) => {
|
||||
let q;
|
||||
|
||||
try {
|
||||
resolve(...args);
|
||||
} finally {
|
||||
q = this._onaddstreamQueue;
|
||||
this._onaddstreamQueue = undefined;
|
||||
}
|
||||
|
||||
this._invokeQueuedOnaddstream(q);
|
||||
}, (...args) => {
|
||||
this._onaddstreamQueue = undefined;
|
||||
|
||||
reject(...args);
|
||||
});
|
||||
|
||||
/* eslint-enable no-invalid-this */
|
||||
});
|
||||
}
|
||||
|
||||
// XXX The function _synthesizeIPv6FromIPv4Address is not placed relative to the
|
||||
// other functions in the file according to alphabetical sorting rule of the
|
||||
// coding style. But eslint wants constants to be defined before they are used.
|
||||
|
||||
/**
|
||||
* Synthesizes an IPv6 address from a specific IPv4 address.
|
||||
*
|
||||
* @param {string} ipv4 - The IPv4 address from which an IPv6 address is to be
|
||||
* synthesized.
|
||||
* @returns {Promise<?string>} A {@code Promise} which gets resolved with the
|
||||
* IPv6 address synthesized from the specified {@code ipv4} or a falsy value to
|
||||
* be treated as inability to synthesize an IPv6 address from the specified
|
||||
* {@code ipv4}.
|
||||
*/
|
||||
const _synthesizeIPv6FromIPv4Address: string => Promise<?string> = (function() {
|
||||
// POSIX.getaddrinfo
|
||||
const { POSIX } = NativeModules;
|
||||
|
||||
if (POSIX) {
|
||||
const { getaddrinfo } = POSIX;
|
||||
|
||||
if (typeof getaddrinfo === 'function') {
|
||||
return ipv4 =>
|
||||
getaddrinfo(/* hostname */ ipv4, /* servname */ undefined)
|
||||
.then(([ { ai_addr: ipv6 } ]) => ipv6);
|
||||
}
|
||||
/**
|
||||
* Synthesize IPv6 addresses before calling the underlying setRemoteDescription.
|
||||
*
|
||||
* @param {Object} description - SDP.
|
||||
* @returns {Promise<undefined>} A promise which is resolved once the operation is complete.
|
||||
*/
|
||||
async setRemoteDescription(description: Object) {
|
||||
return super.setRemoteDescription(await synthesizeIPv6Addresses(description));
|
||||
}
|
||||
|
||||
// NAT64AddrInfo.getIPv6Address
|
||||
const { NAT64AddrInfo } = NativeModules;
|
||||
|
||||
if (NAT64AddrInfo) {
|
||||
const { getIPv6Address } = NAT64AddrInfo;
|
||||
|
||||
if (typeof getIPv6Address === 'function') {
|
||||
return getIPv6Address;
|
||||
}
|
||||
}
|
||||
|
||||
// There's no POSIX.getaddrinfo or NAT64AddrInfo.getIPv6Address.
|
||||
return () =>
|
||||
Promise.reject(
|
||||
'The impossible just happened! No POSIX.getaddrinfo or'
|
||||
+ ' NAT64AddrInfo.getIPv6Address!');
|
||||
})();
|
||||
|
||||
/**
|
||||
* Synthesizes IPv6 addresses on iOS in order to support IPv6 NAT64 networks.
|
||||
*
|
||||
* @param {RTCSessionDescription} sdp - The RTCSessionDescription which
|
||||
* specifies the configuration of the remote end of the connection.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _synthesizeIPv6Addresses(sdp) {
|
||||
return (
|
||||
new Promise(resolve => resolve(_synthesizeIPv6Addresses0(sdp)))
|
||||
.then(({ ips, lines }) =>
|
||||
Promise.all(Array.from(ips.values()))
|
||||
.then(() => _synthesizeIPv6Addresses1(sdp, ips, lines))
|
||||
));
|
||||
}
|
||||
|
||||
/* eslint-disable max-depth */
|
||||
|
||||
/**
|
||||
* Begins the asynchronous synthesis of IPv6 addresses.
|
||||
*
|
||||
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
|
||||
* for which IPv6 addresses will be synthesized.
|
||||
* @private
|
||||
* @returns {{
|
||||
* ips: Map,
|
||||
* lines: Array
|
||||
* }}
|
||||
*/
|
||||
function _synthesizeIPv6Addresses0(sessionDescription) {
|
||||
const sdp = sessionDescription.sdp;
|
||||
let start = 0;
|
||||
const lines = [];
|
||||
const ips = new Map();
|
||||
|
||||
do {
|
||||
const end = sdp.indexOf('\r\n', start);
|
||||
let line;
|
||||
|
||||
if (end === -1) {
|
||||
line = sdp.substring(start);
|
||||
|
||||
// Break out of the loop at the end of the iteration.
|
||||
start = undefined;
|
||||
} else {
|
||||
line = sdp.substring(start, end);
|
||||
start = end + 2;
|
||||
}
|
||||
|
||||
if (line.startsWith('a=candidate:')) {
|
||||
const candidate = line.split(' ');
|
||||
|
||||
if (candidate.length >= 10 && candidate[6] === 'typ') {
|
||||
const ip4s = [ candidate[4] ];
|
||||
let abort = false;
|
||||
|
||||
for (let i = 8; i < candidate.length; ++i) {
|
||||
if (candidate[i] === 'raddr') {
|
||||
ip4s.push(candidate[++i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const ip of ip4s) {
|
||||
if (ip.indexOf(':') === -1) {
|
||||
ips.has(ip)
|
||||
|| ips.set(ip, new Promise((resolve, reject) => {
|
||||
const v = ips.get(ip);
|
||||
|
||||
if (v && typeof v === 'string') {
|
||||
resolve(v);
|
||||
} else {
|
||||
_synthesizeIPv6FromIPv4Address(ip).then(
|
||||
value => {
|
||||
if (!value
|
||||
|| value.indexOf(':') === -1
|
||||
|| value === ips.get(ip)) {
|
||||
ips.delete(ip);
|
||||
} else {
|
||||
ips.set(ip, value);
|
||||
}
|
||||
resolve(value);
|
||||
},
|
||||
reject);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
abort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (abort) {
|
||||
ips.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
line = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(line);
|
||||
} while (start);
|
||||
|
||||
return {
|
||||
ips,
|
||||
lines
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-enable max-depth */
|
||||
|
||||
/**
|
||||
* Completes the asynchronous synthesis of IPv6 addresses.
|
||||
*
|
||||
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
|
||||
* for which IPv6 addresses are being synthesized.
|
||||
* @param {Map} ips - A Map of IPv4 addresses found in the specified
|
||||
* sessionDescription to synthesized IPv6 addresses.
|
||||
* @param {Array} lines - The lines of the specified sessionDescription.
|
||||
* @private
|
||||
* @returns {RTCSessionDescription} A RTCSessionDescription that represents the
|
||||
* result of the synthesis of IPv6 addresses.
|
||||
*/
|
||||
function _synthesizeIPv6Addresses1(sessionDescription, ips, lines) {
|
||||
if (ips.size === 0) {
|
||||
return sessionDescription;
|
||||
}
|
||||
|
||||
for (let l = 0; l < lines.length; ++l) {
|
||||
const candidate = lines[l];
|
||||
|
||||
if (typeof candidate !== 'string') {
|
||||
let ip4 = candidate[4];
|
||||
let ip6 = ips.get(ip4);
|
||||
|
||||
ip6 && (candidate[4] = ip6);
|
||||
|
||||
for (let i = 8; i < candidate.length; ++i) {
|
||||
if (candidate[i] === 'raddr') {
|
||||
ip4 = candidate[++i];
|
||||
(ip6 = ips.get(ip4)) && (candidate[i] = ip6);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lines[l] = candidate.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return new RTCSessionDescription({
|
||||
sdp: lines.join('\r\n'),
|
||||
type: sessionDescription.type
|
||||
});
|
||||
}
|
||||
|
||||
199
react/features/mobile/polyfills/ipv6utils.js
Normal file
199
react/features/mobile/polyfills/ipv6utils.js
Normal file
@@ -0,0 +1,199 @@
|
||||
// @flow
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
import { RTCSessionDescription } from 'react-native-webrtc';
|
||||
|
||||
/**
|
||||
* Synthesizes IPv6 addresses on iOS in order to support IPv6 NAT64 networks.
|
||||
*
|
||||
* @param {RTCSessionDescription} sdp - The RTCSessionDescription which
|
||||
* specifies the configuration of the remote end of the connection.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function synthesizeIPv6Addresses(sdp: RTCSessionDescription) {
|
||||
return (
|
||||
new Promise(resolve => resolve(_synthesizeIPv6Addresses0(sdp)))
|
||||
.then(({ ips, lines }) =>
|
||||
Promise.all(Array.from(ips.values()))
|
||||
.then(() => _synthesizeIPv6Addresses1(sdp, ips, lines))
|
||||
));
|
||||
}
|
||||
|
||||
/* eslint-disable max-depth */
|
||||
|
||||
/**
|
||||
* Synthesizes an IPv6 address from a specific IPv4 address.
|
||||
*
|
||||
* @param {string} ipv4 - The IPv4 address from which an IPv6 address is to be
|
||||
* synthesized.
|
||||
* @returns {Promise<?string>} A {@code Promise} which gets resolved with the
|
||||
* IPv6 address synthesized from the specified {@code ipv4} or a falsy value to
|
||||
* be treated as inability to synthesize an IPv6 address from the specified
|
||||
* {@code ipv4}.
|
||||
*/
|
||||
const _synthesizeIPv6FromIPv4Address: string => Promise<?string> = (function() {
|
||||
// POSIX.getaddrinfo
|
||||
const { POSIX } = NativeModules;
|
||||
|
||||
if (POSIX) {
|
||||
const { getaddrinfo } = POSIX;
|
||||
|
||||
if (typeof getaddrinfo === 'function') {
|
||||
return ipv4 =>
|
||||
getaddrinfo(/* hostname */ ipv4, /* servname */ undefined)
|
||||
.then(([ { ai_addr: ipv6 } ]) => ipv6);
|
||||
}
|
||||
}
|
||||
|
||||
// NAT64AddrInfo.getIPv6Address
|
||||
const { NAT64AddrInfo } = NativeModules;
|
||||
|
||||
if (NAT64AddrInfo) {
|
||||
const { getIPv6Address } = NAT64AddrInfo;
|
||||
|
||||
if (typeof getIPv6Address === 'function') {
|
||||
return getIPv6Address;
|
||||
}
|
||||
}
|
||||
|
||||
// There's no POSIX.getaddrinfo or NAT64AddrInfo.getIPv6Address.
|
||||
return ip => Promise.resolve(ip);
|
||||
})();
|
||||
|
||||
/**
|
||||
* Begins the asynchronous synthesis of IPv6 addresses.
|
||||
*
|
||||
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
|
||||
* for which IPv6 addresses will be synthesized.
|
||||
* @private
|
||||
* @returns {{
|
||||
* ips: Map,
|
||||
* lines: Array
|
||||
* }}
|
||||
*/
|
||||
function _synthesizeIPv6Addresses0(sessionDescription) {
|
||||
const sdp = sessionDescription.sdp;
|
||||
let start = 0;
|
||||
const lines = [];
|
||||
const ips = new Map();
|
||||
|
||||
do {
|
||||
const end = sdp.indexOf('\r\n', start);
|
||||
let line;
|
||||
|
||||
if (end === -1) {
|
||||
line = sdp.substring(start);
|
||||
|
||||
// Break out of the loop at the end of the iteration.
|
||||
start = undefined;
|
||||
} else {
|
||||
line = sdp.substring(start, end);
|
||||
start = end + 2;
|
||||
}
|
||||
|
||||
if (line.startsWith('a=candidate:')) {
|
||||
const candidate = line.split(' ');
|
||||
|
||||
if (candidate.length >= 10 && candidate[6] === 'typ') {
|
||||
const ip4s = [ candidate[4] ];
|
||||
let abort = false;
|
||||
|
||||
for (let i = 8; i < candidate.length; ++i) {
|
||||
if (candidate[i] === 'raddr') {
|
||||
ip4s.push(candidate[++i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const ip of ip4s) {
|
||||
if (ip.indexOf(':') === -1) {
|
||||
ips.has(ip)
|
||||
|| ips.set(ip, new Promise((resolve, reject) => {
|
||||
const v = ips.get(ip);
|
||||
|
||||
if (v && typeof v === 'string') {
|
||||
resolve(v);
|
||||
} else {
|
||||
_synthesizeIPv6FromIPv4Address(ip).then(
|
||||
value => {
|
||||
if (!value
|
||||
|| value.indexOf(':') === -1
|
||||
|| value === ips.get(ip)) {
|
||||
ips.delete(ip);
|
||||
} else {
|
||||
ips.set(ip, value);
|
||||
}
|
||||
resolve(value);
|
||||
},
|
||||
reject);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
abort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (abort) {
|
||||
ips.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
line = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(line);
|
||||
} while (start);
|
||||
|
||||
return {
|
||||
ips,
|
||||
lines
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-enable max-depth */
|
||||
|
||||
/**
|
||||
* Completes the asynchronous synthesis of IPv6 addresses.
|
||||
*
|
||||
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
|
||||
* for which IPv6 addresses are being synthesized.
|
||||
* @param {Map} ips - A Map of IPv4 addresses found in the specified
|
||||
* sessionDescription to synthesized IPv6 addresses.
|
||||
* @param {Array} lines - The lines of the specified sessionDescription.
|
||||
* @private
|
||||
* @returns {RTCSessionDescription} A RTCSessionDescription that represents the
|
||||
* result of the synthesis of IPv6 addresses.
|
||||
*/
|
||||
function _synthesizeIPv6Addresses1(sessionDescription, ips, lines) {
|
||||
if (ips.size === 0) {
|
||||
return sessionDescription;
|
||||
}
|
||||
|
||||
for (let l = 0; l < lines.length; ++l) {
|
||||
const candidate = lines[l];
|
||||
|
||||
if (typeof candidate !== 'string') {
|
||||
let ip4 = candidate[4];
|
||||
let ip6 = ips.get(ip4);
|
||||
|
||||
ip6 && (candidate[4] = ip6);
|
||||
|
||||
for (let i = 8; i < candidate.length; ++i) {
|
||||
if (candidate[i] === 'raddr') {
|
||||
ip4 = candidate[++i];
|
||||
(ip6 = ips.get(ip4)) && (candidate[i] = ip6);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lines[l] = candidate.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return new RTCSessionDescription({
|
||||
sdp: lines.join('\r\n'),
|
||||
type: sessionDescription.type
|
||||
});
|
||||
}
|
||||
@@ -116,7 +116,7 @@ class Notification extends AbstractNotification<Props> {
|
||||
}
|
||||
];
|
||||
|
||||
if (!hideErrorSupportLink) {
|
||||
if (!hideErrorSupportLink && interfaceConfig.SUPPORT_URL) {
|
||||
buttons.push({
|
||||
content: this.props.t('dialog.contactSupport'),
|
||||
onClick: this._onOpenSupportLink
|
||||
|
||||
@@ -7,3 +7,8 @@ export const PARTICIPANTS_PANE_CLOSE = 'PARTICIPANTS_PANE_CLOSE';
|
||||
* Action type to signal the opening of the participants pane.
|
||||
*/
|
||||
export const PARTICIPANTS_PANE_OPEN = 'PARTICIPANTS_PANE_OPEN';
|
||||
|
||||
/**
|
||||
* Action type to set the volume of the participant.
|
||||
*/
|
||||
export const SET_VOLUME = 'SET_VOLUME';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
PARTICIPANTS_PANE_CLOSE,
|
||||
PARTICIPANTS_PANE_OPEN
|
||||
50
react/features/participants-pane/actions.native.js
Normal file
50
react/features/participants-pane/actions.native.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// @flow
|
||||
|
||||
import { openDialog } from '../base/dialog';
|
||||
|
||||
import { SET_VOLUME } from './actionTypes';
|
||||
import {
|
||||
ContextMenuMeetingParticipantDetails,
|
||||
ContextMenuLobbyParticipantReject
|
||||
} from './components/native';
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Displays the context menu for the selected lobby participant.
|
||||
*
|
||||
* @param {Object} participant - The selected lobby participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showContextMenuReject(participant: Object) {
|
||||
return openDialog(ContextMenuLobbyParticipantReject, { participant });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays the context menu for the selected meeting participant.
|
||||
*
|
||||
* @param {string} participantID - The selected meeting participant id.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showContextMenuDetails(participantID: String) {
|
||||
return openDialog(ContextMenuMeetingParticipantDetails, { participantID });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume.
|
||||
*
|
||||
* @param {string} participantId - The participant ID associated with the audio.
|
||||
* @param {string} volume - The volume level.
|
||||
* @returns {{
|
||||
* type: SET_VOLUME,
|
||||
* participantId: string,
|
||||
* volume: number
|
||||
* }}
|
||||
*/
|
||||
export function setVolume(participantId: string, volume: number) {
|
||||
return {
|
||||
type: SET_VOLUME,
|
||||
participantId,
|
||||
volume
|
||||
};
|
||||
}
|
||||
3
react/features/participants-pane/actions.web.js
Normal file
3
react/features/participants-pane/actions.web.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './actions.any';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user