mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-13 09:20:17 +00:00
Compare commits
41 Commits
5166
...
enable-deb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1433a1ee5d | ||
|
|
834ee22bc3 | ||
|
|
d6b5687828 | ||
|
|
6b496d4def | ||
|
|
22cc56ce8d | ||
|
|
0419c5a15b | ||
|
|
dda1f3c5ba | ||
|
|
6f41ef75d7 | ||
|
|
46cbc0ff49 | ||
|
|
53a695da90 | ||
|
|
8bbee7d1dc | ||
|
|
72d4aa7dd5 | ||
|
|
8161309e28 | ||
|
|
465e7f1458 | ||
|
|
4e43a31ec9 | ||
|
|
70c5ea04b1 | ||
|
|
ca25be7314 | ||
|
|
3c2ad24652 | ||
|
|
e421a119e1 | ||
|
|
619acaca24 | ||
|
|
bc9f5773fb | ||
|
|
d0be8dcf9d | ||
|
|
af9958ad66 | ||
|
|
efc5c9dabe | ||
|
|
d22fc88ae3 | ||
|
|
9ee75038b6 | ||
|
|
09af88088d | ||
|
|
2e539ba010 | ||
|
|
87b3ec2cc0 | ||
|
|
907b51925d | ||
|
|
643340c4a6 | ||
|
|
d6c821d524 | ||
|
|
eb16f93153 | ||
|
|
47576aebba | ||
|
|
bac0a55421 | ||
|
|
1c8103c444 | ||
|
|
4e83e93eb6 | ||
|
|
0f8fa4f059 | ||
|
|
becaf0806a | ||
|
|
5b77d722d7 | ||
|
|
f4cde2192e |
@@ -25,5 +25,5 @@ android.enableDexingArtifactTransform.desugaring=false
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
appVersion=21.2.0
|
||||
sdkVersion=3.7.0
|
||||
appVersion=21.3.0
|
||||
sdkVersion=3.8.0
|
||||
|
||||
@@ -70,11 +70,13 @@ 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-video')
|
||||
implementation project(':react-native-webrtc')
|
||||
implementation project(':react-native-webview')
|
||||
implementation project(':react-native-splash-screen')
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
@@ -187,9 +187,11 @@ 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(),
|
||||
new com.brentvatne.react.ReactVideoPackage(),
|
||||
new ReactPackageAdapter() {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
|
||||
@@ -19,13 +19,17 @@ 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'
|
||||
project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
|
||||
include ':react-native-svg'
|
||||
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
|
||||
include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
|
||||
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')
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
conferenceWillJoin,
|
||||
conferenceWillLeave,
|
||||
dataChannelOpened,
|
||||
getConferenceOptions,
|
||||
kickedOut,
|
||||
lockStateChanged,
|
||||
onStartMutedPolicyChanged,
|
||||
@@ -111,7 +112,6 @@ import {
|
||||
trackRemoved
|
||||
} from './react/features/base/tracks';
|
||||
import { downloadJSON } from './react/features/base/util/downloadJSON';
|
||||
import { getConferenceOptions } from './react/features/conference/functions';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker';
|
||||
import { appendSuffix } from './react/features/display-name';
|
||||
import {
|
||||
@@ -132,6 +132,7 @@ import { setScreenAudioShareState, isScreenAudioShared } from './react/features/
|
||||
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
|
||||
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { createPresenterEffect } from './react/features/stream-effects/presenter';
|
||||
import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise';
|
||||
import { endpointMessageReceived } from './react/features/subtitles';
|
||||
import UIEvents from './service/UI/UIEvents';
|
||||
|
||||
@@ -1357,7 +1358,11 @@ export default {
|
||||
},
|
||||
|
||||
_getConferenceOptions() {
|
||||
return getConferenceOptions(APP.store.getState());
|
||||
const options = getConferenceOptions(APP.store.getState());
|
||||
|
||||
options.createVADProcessor = createRnnoiseProcessor;
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
39
config.js
39
config.js
@@ -71,7 +71,7 @@ var config = {
|
||||
},
|
||||
|
||||
// Enables reactions feature.
|
||||
enableReactions: false,
|
||||
// enableReactions: false,
|
||||
|
||||
// Disables ICE/UDP by filtering out local and remote UDP candidates in
|
||||
// signalling.
|
||||
@@ -462,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', 'participants-pane', '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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ target 'JitsiMeetSDK' do
|
||||
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-video', :path => '../node_modules/react-native-video/react-native-video.podspec'
|
||||
pod 'react-native-webview', :path => '../node_modules/react-native-webview'
|
||||
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
|
||||
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage'
|
||||
|
||||
@@ -288,7 +288,12 @@ PODS:
|
||||
- React
|
||||
- react-native-splash-screen (3.2.0):
|
||||
- React
|
||||
- react-native-webrtc (1.89.1):
|
||||
- react-native-video (5.1.1):
|
||||
- React-Core
|
||||
- react-native-video/Video (= 5.1.1)
|
||||
- react-native-video/Video (5.1.1):
|
||||
- React-Core
|
||||
- react-native-webrtc (1.92.0):
|
||||
- React-Core
|
||||
- react-native-webview (11.0.2):
|
||||
- React-Core
|
||||
@@ -346,8 +351,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 +399,7 @@ DEPENDENCIES:
|
||||
- "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-video (from `../node_modules/react-native-video/react-native-video.podspec`)
|
||||
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||
@@ -478,6 +484,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/@react-native-community/slider"
|
||||
react-native-splash-screen:
|
||||
:path: "../node_modules/react-native-splash-screen"
|
||||
react-native-video:
|
||||
:path: "../node_modules/react-native-video/react-native-video.podspec"
|
||||
react-native-webrtc:
|
||||
:path: "../node_modules/react-native-webrtc"
|
||||
react-native-webview:
|
||||
@@ -559,7 +567,8 @@ SPEC CHECKSUMS:
|
||||
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
|
||||
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
|
||||
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
|
||||
react-native-webrtc: ccb0c21eb4fb04326648fbdb4a5d49977e2cf274
|
||||
react-native-video: 1574074179ecaf6a9dd067116c8f31bf9fec15c8
|
||||
react-native-webrtc: bbb644859dcc37ccb7edaec860ca62ed47bf996c
|
||||
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
|
||||
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
|
||||
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6
|
||||
@@ -571,7 +580,7 @@ SPEC CHECKSUMS:
|
||||
React-RCTText: 4f1b99f228278d2a5e9008eced8dc9c974c4a270
|
||||
React-RCTVibration: c1041024893fdfdb8371e7c720c437751b711676
|
||||
ReactCommon: 18014e1d98dbeb9141e935cfe35fc93bd511ffb6
|
||||
RNCAsyncStorage: bc2f81cc1df90c267ce9ed30bb2dbc93b945a8ee
|
||||
RNCAsyncStorage: 8324611026e8dc3706f829953aa6e3899f581589
|
||||
RNDefaultPreference: 56a405ce61033ac77b95004dccd7ac54c2eb50d1
|
||||
RNDeviceInfo: 72ded653ce636b3f03571e90bed99309a714944e
|
||||
RNGoogleSignin: 39336070b35fc4cea6a98cf111e00480317be0ae
|
||||
@@ -580,6 +589,6 @@ SPEC CHECKSUMS:
|
||||
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
|
||||
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
|
||||
|
||||
PODFILE CHECKSUM: 1fa5a1e259f145d32c1ca968b26dac65cff34b49
|
||||
PODFILE CHECKSUM: f4db44d934caeae7212dbaa33abe62ed164363e8
|
||||
|
||||
COCOAPODS: 1.10.1
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.2.0</string>
|
||||
<string>21.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.2.0</string>
|
||||
<string>21.3.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.2.0</string>
|
||||
<string>21.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.2.0</string>
|
||||
<string>21.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.7.0</string>
|
||||
<string>3.8.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -59,6 +59,16 @@
|
||||
|
||||
#pragma mark - Utility methods
|
||||
|
||||
/**
|
||||
* Once the react native bridge is destroyed you are responsible for reinstantiating it back. Use this method to do so.
|
||||
*/
|
||||
- (void)instantiateReactNativeBridge;
|
||||
|
||||
/**
|
||||
* Helper method to destroy the react native bridge, cleaning up resources in the process. Once the react native bridge is destroyed you are responsible for reinstantiating it back using `instantiateReactNativeBridge` method.
|
||||
*/
|
||||
- (void)destroyReactNativeBridge;
|
||||
|
||||
- (JitsiMeetConferenceOptions *_Nonnull)getInitialConferenceOptions;
|
||||
|
||||
- (BOOL)isCrashReportingDisabled;
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#import <RNGoogleSignin/RNGoogleSignin.h>
|
||||
#import <WebRTC/RTCLogging.h>
|
||||
|
||||
|
||||
@implementation JitsiMeet {
|
||||
RCTBridgeWrapper *_bridgeWrapper;
|
||||
NSDictionary *_launchOptions;
|
||||
@@ -50,7 +49,7 @@
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
// Initialize the on and only bridge for interfacing with React Native.
|
||||
// Initialize the one and only bridge for interfacing with React Native.
|
||||
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
|
||||
|
||||
// Initialize the listener for handling start/stop screensharing notifications.
|
||||
@@ -119,6 +118,18 @@
|
||||
|
||||
#pragma mark - Utility methods
|
||||
|
||||
- (void)instantiateReactNativeBridge {
|
||||
if (_bridgeWrapper != nil) {
|
||||
return;
|
||||
};
|
||||
|
||||
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
|
||||
}
|
||||
|
||||
- (void)destroyReactNativeBridge {
|
||||
_bridgeWrapper = nil;
|
||||
}
|
||||
|
||||
- (JitsiMeetConferenceOptions *)getInitialConferenceOptions {
|
||||
if (_launchOptions[UIApplicationLaunchOptionsURLKey]) {
|
||||
NSURL *url = _launchOptions[UIApplicationLaunchOptionsURLKey];
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"shareInvite": "Einladung zur Versammlung teilen",
|
||||
"shareLink": "Teilen Sie den Konferenzlink, um andere einzuladen",
|
||||
"shareStream": "Den Livestreaminglink freigeben",
|
||||
"sip": "SIP: {{address}}",
|
||||
"sipAddresses": "SIP-Adressen",
|
||||
"telephone": "Telefon: {{number}}",
|
||||
"title": "Personen zu dieser Konferenz einladen",
|
||||
"yahooEmail": "Yahoo-E-Mail"
|
||||
@@ -215,6 +215,7 @@
|
||||
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
||||
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
|
||||
"grantModeratorTitle": "Moderationsrechte vergeben",
|
||||
"hideShareAudioHelper": "Diese Meldung nicht mehr anzeigen",
|
||||
"IamHost": "Ich leite das Meeting",
|
||||
"incorrectRoomLockPassword": "Falsches Passwort",
|
||||
"incorrectPassword": "Name oder Passwort ungültig",
|
||||
@@ -257,16 +258,21 @@
|
||||
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
|
||||
"muteParticipantButton": "Stummschalten",
|
||||
"muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
|
||||
"muteParticipantsVideoDialog": "Wollen Sie die Kamera dieser Person wirklich deaktivieren? Sie können die Kamera nicht wieder aktivieren, die Person kann dies aber jederzeit selbst tun.",
|
||||
"muteParticipantTitle": "Person stummschalten?",
|
||||
"muteParticipantsVideoButton": "Kamera ausschalten",
|
||||
"muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?",
|
||||
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
|
||||
"noDropboxToken": "Kein gültiges Dropbox-Token",
|
||||
"Ok": "OK",
|
||||
"password": "Passwort",
|
||||
"passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
|
||||
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
|
||||
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) nicht unterstützt",
|
||||
"passwordRequired": "$t(lockRoomPasswordUppercase) erforderlich",
|
||||
"permissionErrorTitle": "Berechtigung benötigt",
|
||||
"permissionCameraRequiredError": "Der Zugriff auf die Kamera wird benötigt, um in Videokonferenzen teilzunehmen. Bitte in den Einstellungen zulassen",
|
||||
"permissionMicRequiredError": "Der Zugriff auf das Mikrofon wird benötigt, um an Konferenzen mit Ton teilzunehmen. Bitte in den Einstellungen zulassen",
|
||||
"popupError": "Ihr Browser blockiert Pop-ups von dieser Website. Bitte aktivieren Sie Pop-ups in den Sicherheitseinstellungen des Browsers und versuchen Sie es erneut.",
|
||||
"popupErrorTitle": "Pop-up blockiert",
|
||||
"readMore": "mehr",
|
||||
@@ -300,6 +306,13 @@
|
||||
"sessTerminated": "Konferenz beendet",
|
||||
"sessionRestarted": "Konferenz neugestartet",
|
||||
"Share": "Teilen",
|
||||
"shareAudio": "Fortfahren",
|
||||
"shareAudioTitle" : "Wie kann Audio geteilt werden",
|
||||
"shareAudioWarningTitle": "Sie müssen die Bildschirmfreigabe ausschalten, bevor Sie Audio teilen können",
|
||||
"shareAudioWarningH1": "Wenn Sie Ihr Audio teilen wollen:",
|
||||
"shareAudioWarningD1": "müssen Sie Ihre Bildschirmfreigabe stoppen, bevor Sie Audio teilen können.",
|
||||
"shareAudioWarningD2": "müssen Sie Ihre Bildschirmfreigabe neustarten und die Option \"Audio freigeben\" auswählen.",
|
||||
"shareMediaWarningGenericH2": "Wenn Sie Ihren Bildschirm und Audio teilen wollen",
|
||||
"shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.",
|
||||
"shareVideoTitle": "Video teilen",
|
||||
"shareYourScreen": "Bildschirmfreigabe ein-/ausschalten",
|
||||
@@ -307,6 +320,10 @@
|
||||
"startLiveStreaming": "Livestream starten",
|
||||
"startRecording": "Aufnahme starten",
|
||||
"startRemoteControlErrorMessage": "Beim Versuch, die Fernsteuerung zu starten, ist ein Fehler aufgetreten!",
|
||||
"shareScreenWarningTitle": "Sie müssen die Audiofreigabe beenden, bevor Sie den Bildschirm freigeben können",
|
||||
"shareScreenWarningH1": "Wenn Sie Ihren Bildschirm freigeben wollen:",
|
||||
"shareScreenWarningD1": "müssen Sie Ihre Audiofreigabe stoppen, bevor Sie ihren Bildschirm freigeben.",
|
||||
"shareScreenWarningD2": "müssen Sie Ihre Audiofreigabe stoppen und dann die Bildschirmfreigabe mit der Option \"Audio freigeben\" starten.",
|
||||
"stopLiveStreaming": "Livestream stoppen",
|
||||
"stopRecording": "Aufnahme stoppen",
|
||||
"stopRecordingWarning": "Sind Sie sicher, dass Sie die Aufnahme stoppen möchten?",
|
||||
@@ -323,6 +340,9 @@
|
||||
"userIdentifier": "Benutzername",
|
||||
"userPassword": "Passwort",
|
||||
"videoLink": "Video-Link",
|
||||
"viewUpgradeOptions": "Upgradeoptionen anzeigen",
|
||||
"viewUpgradeOptionsContent": "Sie müssen Ihren Tarif erweitern, um Premium-Features wie Aufnahme, Transkription, RTMP-Streaming und mehr zu nutzen.",
|
||||
"viewUpgradeOptionsTitle": "Sie haben ein Premium-Feature entdeckt!",
|
||||
"WaitForHostMsg": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
||||
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
||||
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
|
||||
@@ -349,7 +369,6 @@
|
||||
"blur": "Hintergrund unscharf",
|
||||
"slightBlur": "Hintergrund leicht unscharf",
|
||||
"removeBackground": "Hintergrund entfernen",
|
||||
"uploadImage": "Bild hochladen",
|
||||
"addBackground": "Hintergrund hinzufügen",
|
||||
"pleaseWait": "Bitte warten...",
|
||||
"none": "keiner",
|
||||
@@ -363,7 +382,8 @@
|
||||
"image6" : "Wald",
|
||||
"image7" : "Sonnenaufgang",
|
||||
"desktopShareError": "Desktop konnte nicht freigegeben werden",
|
||||
"desktopShare":"Desktopfreigabe"
|
||||
"desktopShare": "Desktopfreigabe",
|
||||
"webAssemblyWarning": "WebAssembly wird nicht unterstützt"
|
||||
},
|
||||
"feedback": {
|
||||
"average": "Durchschnittlich",
|
||||
@@ -400,6 +420,10 @@
|
||||
"invitePhone": "Wenn Sie stattdessen per Telefon beitreten möchten, wählen sie: {{number}},,{{conferenceID}}#\n",
|
||||
"invitePhoneAlternatives": "Suchen Sie nach einer anderen Einwahlnummer ?\nEinwahlnummern der Konferenz anzeigen: {{url}}\n\n\nWenn Sie sich auch über ein Raumtelefon einwählen, nehmen Sie teil, ohne sich mit dem Ton zu verbinden: {{silentUrl}}",
|
||||
"inviteSipEndpoint": "Um mit SIP teilzunehmen, folgende Adresse nutzen: {{sipUri}}",
|
||||
"inviteTextiOSPersonal": "{{name}} lädt Sie zu einem Meeting ein.",
|
||||
"inviteTextiOSJoinSilent": "Wenn Sie über ein Konferenztelefon teilnehmen, können Sie diesen Link nutzen um ohne Ton an der Konferenz teilzunehmen: {{silentUrl}}.",
|
||||
"inviteTextiOSInviteUrl": "Am Meeting teilnehmen: {{inviteUrl}}.",
|
||||
"inviteTextiOSPhone": "Nutzen Sie folgende Nummer um via Telefon teilzunehmen: {{number}},,{{conferenceID}}#. Wenn Sie nach einer anderen Einwahlnummer suchen, finden Sie die vollständige Liste hier: {{didUrl}}.",
|
||||
"inviteURLFirstPartGeneral": "Sie wurden zur Teilnahme an einem Meeting eingeladen.",
|
||||
"inviteURLFirstPartPersonal": "{{name}} lädt Sie zu einem Meeting ein.\n",
|
||||
"inviteURLSecondPart": "\nAm Meeting teilnehmen:\n{{url}}\n",
|
||||
@@ -410,6 +434,7 @@
|
||||
"noRoom": "Keine Konferenz für die Einwahlinformationen angegeben.",
|
||||
"numbers": "Einwahlnummern",
|
||||
"password": "$t(lockRoomPasswordUppercase):",
|
||||
"sip": "SIP-Adresse",
|
||||
"title": "Teilen",
|
||||
"tooltip": "Freigabe-Link und Einwahlinformationen für dieses Meeting",
|
||||
"label": "Einwahlinformationen"
|
||||
@@ -520,6 +545,7 @@
|
||||
"focus": "Konferenzleitung",
|
||||
"focusFail": "{{component}} ist im Moment nicht verfügbar – wiederholen in {{ms}} Sekunden",
|
||||
"grantedTo": "Moderationsrechte an {{to}} vergeben!",
|
||||
"hostAskedUnmute": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
|
||||
"invitedOneMember": "{{name}} wurde eingeladen",
|
||||
"invitedThreePlusMembers": "{{name}} und {{count}} andere wurden eingeladen",
|
||||
"invitedTwoMembers": "{{first}} und {{second}} wurden eingeladen",
|
||||
@@ -535,7 +561,7 @@
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
|
||||
"raisedHand": "{{name}} möchte sprechen.",
|
||||
"screenShareNoAudio": " Share audio box was not checked in the window selection screen.",
|
||||
"screenShareNoAudio": "Die Option \"Audio freigeben\" wurde bei der Auswahl des Fensters nicht ausgewählt.",
|
||||
"screenShareNoAudioTitle": "Share audio was not checked",
|
||||
"somebody": "Jemand",
|
||||
"startSilentTitle": "Sie sind ohne Audioausgabe beigetreten!",
|
||||
@@ -550,28 +576,40 @@
|
||||
"oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
|
||||
"oldElectronClientDescription2": "aktuelle Version",
|
||||
"oldElectronClientDescription3": "!",
|
||||
"moderationInEffectDescription": "Bitte melden um zu sprechen",
|
||||
"moderationInEffectCSDescription": "Bitte melden um ein Video zu teilen",
|
||||
"moderationInEffectVideoDescription": "Bitte melden um die Kamera zu starten",
|
||||
"moderationInEffectTitle": "Das Mikrofon ist von der Moderation gesperrt",
|
||||
"moderationInEffectCSTitle": "Die Videofreigabe ist von der Moderation gesperrt",
|
||||
"moderationInEffectVideoTitle": "Die Kamera ist von der Moderation gesperrt",
|
||||
"moderationRequestFromModerator": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
|
||||
"moderationRequestFromParticipant": "möchte sprechen",
|
||||
"moderationStartedTitle": "Moderation gestartet",
|
||||
"moderationStoppedTitle": "Moderation gestoppt",
|
||||
"moderationToggleDescription": "von {{participantDisplayName}}",
|
||||
"raiseHandAction": "Melden",
|
||||
"groupTitle": "Benachrichtigungen"
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "Schließen",
|
||||
"header": "Anwesende",
|
||||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Teilnehmer ({{count}})"
|
||||
},
|
||||
"actions": {
|
||||
"muteAll": "Alle stummschalten",
|
||||
"stopVideo": "Video stoppen"
|
||||
}
|
||||
},
|
||||
"participantsPane": {
|
||||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Anwesende ({{count}})"
|
||||
"participantsList": "Anwesende ({{count}})",
|
||||
"waitingLobby": "In der Lobby ({{count}})"
|
||||
},
|
||||
"actions": {
|
||||
"allow": "Anwesenden erlauben:",
|
||||
"blockEveryoneMicCamera": "Kamera und Mikrofon von allen sperren",
|
||||
"invite": "Person einladen",
|
||||
"askUnmute": "Anfragen, Stummschaltung aufzuheben",
|
||||
"mute": "Stummschalten",
|
||||
"muteAll": "Alle stummschalten",
|
||||
"stopVideo": "Kamera ausschalten"
|
||||
"muteEveryoneElse": "Alle anderen stummschalten",
|
||||
"startModeration": "Stummschaltung aufheben oder Kamera aktivieren",
|
||||
"stopEveryonesVideo": "Alle Kameras ausschalten",
|
||||
"stopVideo": "Kamera ausschalten",
|
||||
"unblockEveryoneMicCamera": "Kamera und Mikrofon von allen entsperren"
|
||||
}
|
||||
},
|
||||
"passwordSetRemotely": "von einer anderen Person gesetzt",
|
||||
@@ -626,9 +664,9 @@
|
||||
"linkCopied": "Link in die Zwischenablage kopiert",
|
||||
"lookGood": "Ihr Mikrofon scheint zu funktionieren.",
|
||||
"or": "oder",
|
||||
"keyboardShortcuts" : "Tastaturkurzbefehle aktivieren",
|
||||
"premeeting": "Vorschau",
|
||||
"showScreen": "Konferenzvorschau aktivieren",
|
||||
"keyboardShortcuts" : "Tastaturkurzbefehle aktivieren",
|
||||
"startWithPhone": "Mit Telefonaudio starten",
|
||||
"screenSharingError": "Fehler bei Bildschirmfreigabe:",
|
||||
"videoOnlyError": "Videofehler:",
|
||||
@@ -665,12 +703,15 @@
|
||||
"beta": "BETA",
|
||||
"busy": "Es werden Ressourcen für eine Aufnahme bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
|
||||
"busyTitle": "Alle Aufnahme-Instanzen sind in Gebrauch",
|
||||
"copyLink": "Link kopieren",
|
||||
"error": "Die Aufzeichnung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"errorFetchingLink": "Der Link zur Aufzeichnung konnte nicht geladen werden.",
|
||||
"expandedOff": "Aufzeichnung wurde gestoppt",
|
||||
"expandedOn": "Das Meeting wird momentan aufgezeichnet.",
|
||||
"expandedPending": "Aufzeichnung wird gestartet…",
|
||||
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
|
||||
"fileSharingdescription": "Aufzeichnung mit den Personen der Konferenz teilen",
|
||||
"linkGenerated": "Link zur Aufzeichnung wurde generiert.",
|
||||
"live": "LIVE",
|
||||
"loggedIn": "Als {{userName}} angemeldet",
|
||||
"off": "Aufnahme gestoppt",
|
||||
@@ -685,7 +726,8 @@
|
||||
"signIn": "Anmelden",
|
||||
"signOut": "Abmelden",
|
||||
"unavailable": "Oh! Der {{serviceName}} ist aktuell nicht verfügbar. Wir arbeiten an der Behebung des Problems. Bitte versuchen Sie es später noch einmal.",
|
||||
"unavailableTitle": "Aufnahme nicht verfügbar"
|
||||
"unavailableTitle": "Aufnahme nicht verfügbar",
|
||||
"uploadToCloud": "In die Cloud hochladen"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Ziehen, um zu aktualisieren"
|
||||
@@ -704,8 +746,13 @@
|
||||
"signedIn": "Momentan wird auf Kalendertermine von {{email}} zugegriffen. Klicken Sie auf die folgende Schaltfläche „Trennen“, um den Zugriff auf die Kalendertermine zu stoppen.",
|
||||
"title": "Kalender"
|
||||
},
|
||||
"desktopShareFramerate": "Framerate für Bildschirmfreigabe",
|
||||
"desktopShareWarning": "Sie müssen die Bildschirmfreigabe neustarten, damit die Einstellung übernommen wird.",
|
||||
"desktopShareHighFpsWarning": "Eine höhere Framerate könnte sich auf Ihre Datenrate auswirken. Sie müssen die Bildschirmfreigabe neustarten, damit die Einstellung übernommen wird.",
|
||||
"devices": "Geräte",
|
||||
"followMe": "Follow-me für alle Personen",
|
||||
"framesPerSecond": "FPS",
|
||||
"incomingMessage": "Eingehende Nachricht",
|
||||
"language": "Sprache",
|
||||
"loggedIn": "Als {{name}} angemeldet",
|
||||
"microphones": "Mikrofon",
|
||||
@@ -713,12 +760,18 @@
|
||||
"more": "Mehr",
|
||||
"name": "Name",
|
||||
"noDevice": "Kein",
|
||||
"participantJoined": "Neue Person nimmt teil",
|
||||
"participantLeft": "Person verlässt die Konferenz",
|
||||
"playSounds": "Hinweistöne aktiviert",
|
||||
"sameAsSystem": "Wie System ({{label}})",
|
||||
"selectAudioOutput": "Audioausgabe",
|
||||
"selectCamera": "Kamera",
|
||||
"selectMic": "Mikrofon",
|
||||
"sounds": "Hinweistöne",
|
||||
"speakers": "Lautsprecher",
|
||||
"startAudioMuted": "Alle Personen treten stumm geschaltet bei",
|
||||
"startAudioMuted": "Alle Personen treten stummgeschaltet bei",
|
||||
"startVideoMuted": "Alle Personen treten ohne Video bei",
|
||||
"talkWhileMuted": "Wenn bei Stummschaltung gesprochen wird",
|
||||
"title": "Einstellungen"
|
||||
},
|
||||
"settingsView": {
|
||||
@@ -767,12 +820,14 @@
|
||||
"title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde."
|
||||
},
|
||||
"toolbar": {
|
||||
"accessibilityLabel": {
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "„Nur Audio“ ein-/ausschalten",
|
||||
"audioRoute": "Audiogerät auswählen",
|
||||
"boo": "Buhen",
|
||||
"callQuality": "Qualitätseinstellungen",
|
||||
"cc": "Untertitel ein-/ausschalten",
|
||||
"chat": "Chatfenster öffnen / schließen",
|
||||
"clap": "Klatschen",
|
||||
"document": "Geteiltes Dokument schließen",
|
||||
"download": "Unsere Apps herunterladen",
|
||||
"embedMeeting": "Konferenz einbetten",
|
||||
@@ -783,6 +838,8 @@
|
||||
"help": "Hilfe",
|
||||
"invite": "Person einladen",
|
||||
"kick": "Person entfernen",
|
||||
"laugh": "Lachen",
|
||||
"like": "Daumen nach oben",
|
||||
"lobbyButton": "Lobbymodus ein-/ausschalten",
|
||||
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
|
||||
"lockRoom": "Konferenzpasswort ein-/ausschalten",
|
||||
@@ -795,10 +852,12 @@
|
||||
"muteEveryonesVideo": "Alle Kameras ausschalten",
|
||||
"muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten",
|
||||
"participants": "Anwesende",
|
||||
"party": "Konfetti",
|
||||
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
|
||||
"privateMessage": "Private Nachricht senden",
|
||||
"profile": "Profil bearbeiten",
|
||||
"raiseHand": "Hand erheben / senken",
|
||||
"reactionsMenu": "Interaktionsmenü öffnen / schließen",
|
||||
"recording": "Aufzeichnung ein-/ausschalten",
|
||||
"remoteMute": "Personen stummschalten",
|
||||
"remoteVideoMute": "Kamera von dieser Person ausschalten",
|
||||
@@ -811,23 +870,29 @@
|
||||
"shortcuts": "Tastenkombinationen ein-/ausblenden",
|
||||
"show": "Im Vordergrund anzeigen",
|
||||
"speakerStats": "Sprechstatistik ein-/ausblenden",
|
||||
"surprised": "Überrascht",
|
||||
"tileView": "Kachelansicht ein-/ausschalten",
|
||||
"toggleCamera": "Kamera wechseln",
|
||||
"toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
|
||||
"videomute": "„Video stummschalten“ ein-/ausschalten",
|
||||
"videoblur": "Unscharfer Hintergrund ein-/ausschalten",
|
||||
"selectBackground": "Hintergrund auswählen",
|
||||
"expand": "Ausklappen",
|
||||
"collapse": "Einklappen"
|
||||
},
|
||||
"addPeople": "Personen zur Konferenz hinzufügen",
|
||||
"audioSettings": "Ton-Einstellungen",
|
||||
"videoSettings": "Kameraeinstellungen",
|
||||
"audioOnlyOff": "Modus „Nur Audio“ deaktivieren",
|
||||
"audioOnlyOn": "Modus „Nur Audio“ aktivieren",
|
||||
"audioRoute": "Audiogerät auswählen",
|
||||
"authenticate": "Anmelden",
|
||||
"boo": "Buhen",
|
||||
"callQuality": "Qualitätseinstellungen",
|
||||
"chat": "Chat öffnen / schließen",
|
||||
"clap": "Klatschen",
|
||||
"closeChat": "Chat schließen",
|
||||
"closeReactionsMenu": "Interationsmenü schließen",
|
||||
"documentClose": "Geteiltes Dokument schließen",
|
||||
"documentOpen": "Geteiltes Dokument öffnen",
|
||||
"download": "Unsere Apps herunterladen",
|
||||
@@ -841,6 +906,8 @@
|
||||
"hangup": "Konferenz verlassen",
|
||||
"help": "Hilfe",
|
||||
"invite": "Personen einladen",
|
||||
"laugh": "Lachen",
|
||||
"like": "Daumen hoch",
|
||||
"lobbyButtonDisable": "Lobbymodus deaktivieren",
|
||||
"lobbyButtonEnable": "Lobbymodus aktivieren",
|
||||
"login": "Anmelden",
|
||||
@@ -859,12 +926,20 @@
|
||||
"noisyAudioInputTitle": "Ihr Mikrofon scheint lärmintensiv zu sein!",
|
||||
"noisyAudioInputDesc": "Es klingt, als ob Ihr Mikrofon Störgeräusche verursacht. Bitte überlegen Sie, ob Sie das Gerät stummschalten oder austauschen wollen.",
|
||||
"openChat": "Chat öffnen",
|
||||
"openReactionsMenu": "Interationsmenü öffnen",
|
||||
"participants": "Anwesende",
|
||||
"party": "Konfetti",
|
||||
"pip": "Bild-in-Bild-Modus einschalten",
|
||||
"privateMessage": "Private Nachricht senden",
|
||||
"profile": "Profil bearbeiten",
|
||||
"raiseHand": "Hand erheben / senken",
|
||||
"raiseYourHand": "Melden",
|
||||
"reactionBoo": "Buhen senden",
|
||||
"reactionClap": "Klatschen senden",
|
||||
"reactionLaugh": "Lachen senden",
|
||||
"reactionLike": "Daumen hoch senden",
|
||||
"reactionParty": "Konfetti senden",
|
||||
"reactionSurprised": "Überrascht senden",
|
||||
"security": "Sicherheitsoptionen",
|
||||
"Settings": "Einstellungen",
|
||||
"shareaudio": "Audio teilen",
|
||||
@@ -874,14 +949,15 @@
|
||||
"speakerStats": "Sprechstatistik",
|
||||
"startScreenSharing": "Bildschirmfreigabe starten",
|
||||
"startSubtitles": "Untertitel einschalten",
|
||||
"stopAudioSharing": "Audiofreigabe stoppen",
|
||||
"stopScreenSharing": "Bildschirmfreigabe stoppen",
|
||||
"stopSubtitles": "Untertitel ausschalten",
|
||||
"stopSharedVideo": "YouTube-Video stoppen",
|
||||
"surprised": "Überrascht",
|
||||
"talkWhileMutedPopup": "Versuchen Sie zu sprechen? Ihr Mikrofon ist stummgeschaltet.",
|
||||
"tileViewToggle": "Kachelansicht ein-/ausschalten",
|
||||
"toggleCamera": "Kamera wechseln",
|
||||
"videomute": "Kamera starten / stoppen",
|
||||
"videoSettings": "Video-Einstellungen",
|
||||
"selectBackground": "Hintergrund auswählen"
|
||||
},
|
||||
"transcribing": {
|
||||
@@ -975,10 +1051,10 @@
|
||||
"info": "Einwahlinformationen",
|
||||
"join": "ERSTELLEN / BEITRETEN",
|
||||
"jitsiOnMobile": "Jitsi unterwegs – einfach unsere Apps herunterladen und Meetings von überall starten",
|
||||
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
|
||||
"mobileDownLoadLinkIos": "iOS App Download",
|
||||
"mobileDownLoadLinkAndroid": "Android App Download",
|
||||
"mobileDownLoadLinkFDroid": "F-Droid App Download",
|
||||
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
|
||||
"privacy": "Datenschutz",
|
||||
"recentList": "Verlauf",
|
||||
"recentListDelete": "Eintrag löschen",
|
||||
@@ -1009,6 +1085,7 @@
|
||||
},
|
||||
"lobby": {
|
||||
"admit": "Zulassen",
|
||||
"admitAll": "Alle zulassen",
|
||||
"knockingParticipantList": "Liste anklopfender Personen",
|
||||
"allow": "Annehmen",
|
||||
"backToKnockModeButton": "Kein Passwort, stattdessen Beitritt anfragen",
|
||||
@@ -1039,6 +1116,7 @@
|
||||
"passwordField": "Konferenzpasswort eingeben",
|
||||
"passwordJoinButton": "Beitreten",
|
||||
"reject": "Ablehnen",
|
||||
"rejectAll": "Alle ablehnen",
|
||||
"toggleLabel": "Lobby aktivieren"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -263,6 +263,7 @@
|
||||
"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.",
|
||||
@@ -591,6 +592,7 @@
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "Close",
|
||||
"header": "Participants",
|
||||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Meeting participants ({{count}})",
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
2
modules/API/external/external_api.js
vendored
2
modules/API/external/external_api.js
vendored
@@ -170,7 +170,7 @@ function parseArguments(args) {
|
||||
|
||||
switch (typeof firstArg) {
|
||||
case 'string': // old arguments format
|
||||
case undefined: {
|
||||
case 'undefined': {
|
||||
// Not sure which format but we are trying to parse the old
|
||||
// format because if the new format is used everything will be undefined
|
||||
// anyway.
|
||||
|
||||
61
package-lock.json
generated
61
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"
|
||||
}
|
||||
@@ -7539,6 +7539,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"eme-encryption-scheme-polyfill": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.0.3.tgz",
|
||||
"integrity": "sha512-44CNFMsqzHdKHrzWxlS7xZ8KUHn5XutBqpmCuWzNIynmAyFInHrrD3ozv/RvK9ZhgV6QY6Easx8EWAmxteNodg=="
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",
|
||||
@@ -8244,7 +8249,8 @@
|
||||
"events": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
|
||||
"integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg=="
|
||||
"integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==",
|
||||
"dev": true
|
||||
},
|
||||
"eventsource": {
|
||||
"version": "1.0.7",
|
||||
@@ -11016,6 +11022,11 @@
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
|
||||
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
|
||||
},
|
||||
"keymirror": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz",
|
||||
"integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU="
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
@@ -11076,8 +11087,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#b815157a22cec219a26457143b6d6cb2f430e01b",
|
||||
"from": "github:jitsi/lib-jitsi-meet#b815157a22cec219a26457143b6d6cb2f430e01b",
|
||||
"version": "github:jitsi/lib-jitsi-meet#6a3df11ffa7a2204b579326e23cdaa85a79521d1",
|
||||
"from": "github:jitsi/lib-jitsi-meet#6a3df11ffa7a2204b579326e23cdaa85a79521d1",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.2",
|
||||
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#5fc4af6dcf8a6e6af9fedbcd654412fd47b1b4ae",
|
||||
@@ -15143,14 +15154,25 @@
|
||||
"whatwg-url-without-unicode": "8.0.0-3"
|
||||
}
|
||||
},
|
||||
"react-native-video": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-5.1.1.tgz",
|
||||
"integrity": "sha512-zee8gRUrjPWRoZSEBiMebClqu1iAuCQNLjzqpmXFrRWEoJj7azM3BPqLQWJgsnfLiYUYGySeApC/G60THM5+tw==",
|
||||
"requires": {
|
||||
"keymirror": "^0.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"shaka-player": "^2.5.9"
|
||||
}
|
||||
},
|
||||
"react-native-watch-connectivity": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-0.4.3.tgz",
|
||||
"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.92.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.92.0.tgz",
|
||||
"integrity": "sha512-nztKQ/SmO1DgA3QWCDHHK8ZVDf+5rLbmH42Ukoqnld7ut8/ehmFZXc17aSV/BN0H60jPigGqAMYopt/LZak7Sg==",
|
||||
"requires": {
|
||||
"base64-js": "^1.1.2",
|
||||
"cross-os": "^1.3.0",
|
||||
@@ -15191,11 +15213,18 @@
|
||||
}
|
||||
},
|
||||
"react-native-youtube-iframe": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-1.2.3.tgz",
|
||||
"integrity": "sha512-3O8OFJyohGNlYX4D97aWfLLlhEHhlLHDCLgXM+SsQBwP9r1oLnKgXWoy1gce+Vr8qgrqeQgmx1ki+10AAd4KWQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-2.1.1.tgz",
|
||||
"integrity": "sha512-vnLzA5zcnMwa1gMqGfvkjaE82NW1Nd2Up4Q1OUz6IKm69xSG/9/m4APZ5fCN8UMhy6lH95iagd497J7jwEwz3w==",
|
||||
"requires": {
|
||||
"events": "^3.0.0"
|
||||
"events": "^3.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-node-resolver": {
|
||||
@@ -16069,6 +16098,14 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"shaka-player": {
|
||||
"version": "2.5.22",
|
||||
"resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.22.tgz",
|
||||
"integrity": "sha512-PAoeNLUQ/hT/9dY7QvNFgIiDtXSqbYVFuXXtLHh7ytVVqTvI/p4HLwfYShiR+sE/sbsDOr9D5l9D/ztLPhxgtw==",
|
||||
"requires": {
|
||||
"eme-encryption-scheme-polyfill": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"shallow-clone": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@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",
|
||||
@@ -56,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#b815157a22cec219a26457143b6d6cb2f430e01b",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6a3df11ffa7a2204b579326e23cdaa85a79521d1",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
@@ -87,10 +87,11 @@
|
||||
"react-native-svg": "12.1.0",
|
||||
"react-native-svg-transformer": "0.14.3",
|
||||
"react-native-url-polyfill": "1.2.0",
|
||||
"react-native-video": "5.1.1",
|
||||
"react-native-watch-connectivity": "0.4.3",
|
||||
"react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#510d20dd62c1768885a98f36fde83f9e48a723fa",
|
||||
"react-native-webrtc": "1.92.0",
|
||||
"react-native-webview": "11.0.2",
|
||||
"react-native-youtube-iframe": "1.2.3",
|
||||
"react-native-youtube-iframe": "2.1.1",
|
||||
"react-redux": "7.1.0",
|
||||
"react-textarea-autosize": "8.3.0",
|
||||
"react-transition-group": "2.4.0",
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
createStartMutedConfigurationEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { getName } from '../../app/functions';
|
||||
import { endpointMessageReceived } from '../../subtitles';
|
||||
import { getReplaceParticipant } from '../config/functions';
|
||||
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
|
||||
@@ -14,7 +13,6 @@ import { JitsiConferenceEvents } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, setAudioMuted, setVideoMuted } from '../media';
|
||||
import {
|
||||
dominantSpeakerChanged,
|
||||
getLocalParticipant,
|
||||
getNormalizedDisplayName,
|
||||
participantConnectionStatusChanged,
|
||||
participantKicked,
|
||||
@@ -24,11 +22,7 @@ import {
|
||||
participantUpdated
|
||||
} from '../participants';
|
||||
import { getLocalTracks, replaceLocalTrack, trackAdded, trackRemoved } from '../tracks';
|
||||
import {
|
||||
getBackendSafePath,
|
||||
getBackendSafeRoomName,
|
||||
getJitsiMeetGlobalNS
|
||||
} from '../util';
|
||||
import { getBackendSafeRoomName } from '../util';
|
||||
|
||||
import {
|
||||
AUTH_STATUS_CHANGED,
|
||||
@@ -61,6 +55,7 @@ import {
|
||||
_addLocalTracksToConference,
|
||||
commonUserJoinedHandling,
|
||||
commonUserLeftHandling,
|
||||
getConferenceOptions,
|
||||
getCurrentConference,
|
||||
sendLocalParticipant
|
||||
} from './functions';
|
||||
@@ -434,22 +429,7 @@ export function createConference() {
|
||||
throw new Error('Cannot join a conference without a room name!');
|
||||
}
|
||||
|
||||
const config = state['features/base/config'];
|
||||
const { tenant } = state['features/base/jwt'];
|
||||
const { email, name: nick } = getLocalParticipant(state);
|
||||
|
||||
const conference
|
||||
= connection.initJitsiConference(
|
||||
|
||||
getBackendSafeRoomName(room), {
|
||||
...config,
|
||||
applicationName: getName(),
|
||||
getWiFiStatsMethod: getJitsiMeetGlobalNS().getWiFiStats,
|
||||
confID: `${locationURL.host}${getBackendSafePath(locationURL.pathname)}`,
|
||||
siteID: tenant,
|
||||
statisticsDisplayName: config.enableDisplayNameInStats ? nick : undefined,
|
||||
statisticsId: config.enableEmailInStats ? email : undefined
|
||||
});
|
||||
const conference = connection.initJitsiConference(getBackendSafeRoomName(room), getConferenceOptions(state));
|
||||
|
||||
connection[JITSI_CONNECTION_CONFERENCE_KEY] = conference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { getName } from '../../app/functions';
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
participantLeft
|
||||
} from '../participants';
|
||||
import { toState } from '../redux';
|
||||
import { safeDecodeURIComponent } from '../util';
|
||||
import { getBackendSafePath, getJitsiMeetGlobalNS, safeDecodeURIComponent } from '../util';
|
||||
|
||||
import {
|
||||
AVATAR_URL_COMMAND,
|
||||
@@ -198,6 +199,53 @@ export function getConferenceNameForTitle(stateful: Function | Object) {
|
||||
return safeStartCase(safeDecodeURIComponent(getConferenceState(toState(stateful)).room));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object aggregating the conference options.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux store state.
|
||||
* @returns {Object} - Options object.
|
||||
*/
|
||||
export function getConferenceOptions(stateful: Function | Object) {
|
||||
const state = toState(stateful);
|
||||
|
||||
const config = state['features/base/config'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const { tenant } = state['features/base/jwt'];
|
||||
const { email, name: nick } = getLocalParticipant(state);
|
||||
const options = { ...config };
|
||||
|
||||
if (tenant) {
|
||||
options.siteID = tenant;
|
||||
}
|
||||
|
||||
if (options.enableDisplayNameInStats && nick) {
|
||||
options.statisticsDisplayName = nick;
|
||||
}
|
||||
|
||||
if (options.enableEmailInStats && email) {
|
||||
options.statisticsId = email;
|
||||
}
|
||||
|
||||
if (locationURL) {
|
||||
options.confID = `${locationURL.host}${getBackendSafePath(locationURL.pathname)}`;
|
||||
}
|
||||
|
||||
options.applicationName = getName();
|
||||
|
||||
// Disable analytics, if requessted.
|
||||
if (options.disableThirdPartyRequests) {
|
||||
delete config.analytics.scriptURLs;
|
||||
delete config.analytics.amplitudeAPPKey;
|
||||
delete config.analytics.googleAnalyticsTrackingId;
|
||||
delete options.callStatsID;
|
||||
delete options.callStatsSecret;
|
||||
} else {
|
||||
options.getWiFiStatsMethod = getWiFiStatsMethod;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UTC timestamp when the first participant joined the conference.
|
||||
*
|
||||
@@ -244,6 +292,21 @@ export function getRoomName(state: Object): string {
|
||||
return getConferenceState(state).room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of getWiFiStats from the global NS or does nothing
|
||||
* (returns empty result).
|
||||
* Fixes a concurrency problem where we need to pass a function when creating
|
||||
* a JitsiConference, but that method is added to the context later.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
function getWiFiStatsMethod() {
|
||||
const gloabalNS = getJitsiMeetGlobalNS();
|
||||
|
||||
return gloabalNS.getWiFiStats ? gloabalNS.getWiFiStats() : Promise.resolve('{}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an error thrown by the backend (i.e. {@code lib-jitsi-meet}) while
|
||||
* manipulating a conference participant (e.g. Pin or select participant).
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
@@ -142,7 +142,9 @@ class BottomSheet extends PureComponent<Props> {
|
||||
<SafeAreaView
|
||||
style = { [
|
||||
styles.sheetItemContainer,
|
||||
_styles.sheet,
|
||||
renderHeader
|
||||
? _styles.sheetHeader
|
||||
: _styles.sheet,
|
||||
style,
|
||||
{
|
||||
maxHeight: _height - 100
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
|
||||
import { ColorSchemeRegistry, schemeColor } from '../../../color-scheme';
|
||||
import { BoxModel, ColorPalette } from '../../../styles';
|
||||
import { PREFERRED_DIALOG_SIZE } from '../../constants';
|
||||
@@ -171,7 +172,7 @@ ColorSchemeRegistry.register('BottomSheet', {
|
||||
*/
|
||||
labelStyle: {
|
||||
...brandedDialogLabelStyle,
|
||||
marginLeft: 32
|
||||
marginLeft: 16
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -179,7 +180,6 @@ ColorSchemeRegistry.register('BottomSheet', {
|
||||
*/
|
||||
style: {
|
||||
...brandedDialogItemContainerStyle,
|
||||
backgroundColor: ColorPalette.darkBackground,
|
||||
paddingHorizontal: MD_ITEM_MARGIN_PADDING
|
||||
},
|
||||
|
||||
@@ -193,9 +193,16 @@ ColorSchemeRegistry.register('BottomSheet', {
|
||||
* Bottom sheet's base style.
|
||||
*/
|
||||
sheet: {
|
||||
backgroundColor: ColorPalette.black,
|
||||
backgroundColor: BaseTheme.palette.ui02,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16
|
||||
},
|
||||
|
||||
/**
|
||||
* Bottom sheet's base style with header.
|
||||
*/
|
||||
sheetHeader: {
|
||||
backgroundColor: BaseTheme.palette.ui02
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { YoutubeLargeVideo } from '../../../shared-video/components';
|
||||
import { SharedVideo } from '../../../shared-video/components/native';
|
||||
import { Avatar } from '../../avatar';
|
||||
import { translate } from '../../i18n';
|
||||
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
|
||||
@@ -208,11 +208,11 @@ class ParticipantView extends Component<Props> {
|
||||
? this.props.testHintId
|
||||
: `org.jitsi.meet.Participant#${this.props.participantId}`;
|
||||
|
||||
const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo;
|
||||
const renderSharedVideo = _isFakeParticipant && !disableVideo;
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { renderVideo || renderYoutubeLargeVideo ? undefined : onPress }
|
||||
onClick = { renderVideo || renderSharedVideo ? undefined : onPress }
|
||||
style = {{
|
||||
...styles.participantView,
|
||||
...this.props.style
|
||||
@@ -221,10 +221,10 @@ class ParticipantView extends Component<Props> {
|
||||
|
||||
<TestHint
|
||||
id = { testHintId }
|
||||
onPress = { renderYoutubeLargeVideo ? undefined : onPress }
|
||||
onPress = { renderSharedVideo ? undefined : onPress }
|
||||
value = '' />
|
||||
|
||||
{ renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { this.props.participantId } /> }
|
||||
{ renderSharedVideo && <SharedVideo /> }
|
||||
|
||||
{ !_isFakeParticipant && renderVideo
|
||||
&& <VideoTrack
|
||||
@@ -234,7 +234,7 @@ class ParticipantView extends Component<Props> {
|
||||
zOrder = { this.props.zOrder }
|
||||
zoomEnabled = { this.props.zoomEnabled } /> }
|
||||
|
||||
{ !renderYoutubeLargeVideo && !renderVideo
|
||||
{ !renderSharedVideo && !renderVideo
|
||||
&& <View style = { styles.avatarContainer }>
|
||||
<Avatar
|
||||
participantId = { this.props.participantId }
|
||||
|
||||
@@ -227,7 +227,7 @@ export function noDataFromSource(track) {
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showNoDataFromSourceVideoError(jitsiTrack) {
|
||||
return (dispatch, getState) => {
|
||||
return async (dispatch, getState) => {
|
||||
let notificationInfo;
|
||||
|
||||
const track = getTrackByJitsiTrack(getState()['features/base/tracks'], jitsiTrack);
|
||||
@@ -239,12 +239,11 @@ export function showNoDataFromSourceVideoError(jitsiTrack) {
|
||||
if (track.isReceivingData) {
|
||||
notificationInfo = undefined;
|
||||
} else {
|
||||
const notificationAction = showErrorNotification({
|
||||
const notificationAction = await dispatch(showErrorNotification({
|
||||
descriptionKey: 'dialog.cameraNotSendingData',
|
||||
titleKey: 'dialog.cameraNotSendingDataTitle'
|
||||
});
|
||||
}));
|
||||
|
||||
dispatch(notificationAction);
|
||||
notificationInfo = {
|
||||
uid: notificationAction.uid
|
||||
};
|
||||
@@ -362,7 +361,7 @@ function replaceStoredTracks(oldTrack, newTrack) {
|
||||
* @returns {{ type: TRACK_ADDED, track: Track }}
|
||||
*/
|
||||
export function trackAdded(track) {
|
||||
return (dispatch, getState) => {
|
||||
return async (dispatch, getState) => {
|
||||
track.on(
|
||||
JitsiTrackEvents.TRACK_MUTE_CHANGED,
|
||||
() => dispatch(trackMutedChanged(track)));
|
||||
@@ -389,12 +388,10 @@ export function trackAdded(track) {
|
||||
track.on(JitsiTrackEvents.NO_DATA_FROM_SOURCE, () => dispatch(noDataFromSource({ jitsiTrack: track })));
|
||||
if (!isReceivingData) {
|
||||
if (mediaType === MEDIA_TYPE.AUDIO) {
|
||||
const notificationAction = showNotification({
|
||||
const notificationAction = await dispatch(showNotification({
|
||||
descriptionKey: 'dialog.micNotSendingData',
|
||||
titleKey: 'dialog.micNotSendingDataTitle'
|
||||
});
|
||||
|
||||
dispatch(notificationAction);
|
||||
}));
|
||||
|
||||
// Set the notification ID so that other parts of the application know that this was
|
||||
// displayed in the context of the current device.
|
||||
|
||||
@@ -18,6 +18,7 @@ export const colors = {
|
||||
primary08: '#99BBF3',
|
||||
primary09: '#CCDDF9',
|
||||
|
||||
surface00: '#111111',
|
||||
surface01: '#040404',
|
||||
surface02: '#141414',
|
||||
surface03: '#292929',
|
||||
@@ -29,6 +30,7 @@ export const colors = {
|
||||
surface09: '#C2C2C2',
|
||||
surface10: '#E0E0E0',
|
||||
surface11: '#FFF',
|
||||
surface12: '#AAAAAA',
|
||||
|
||||
success04: '#189B55',
|
||||
success05: '#1EC26A',
|
||||
@@ -109,6 +111,9 @@ export const colorMap = {
|
||||
// Disabled state for danger buttons
|
||||
actionDangerDisabled: 'error03',
|
||||
|
||||
// Bottom sheet background
|
||||
bottomSheet: 'surface00',
|
||||
|
||||
// Primary text – default color for body copy & headers
|
||||
text01: 'surface11',
|
||||
|
||||
@@ -118,6 +123,9 @@ export const colorMap = {
|
||||
// Tertiary text with low contrast – placeholders, disabled actions, label for disabled buttons
|
||||
text03: 'surface07',
|
||||
|
||||
// Text for bottom sheet items
|
||||
text04: 'surface12',
|
||||
|
||||
// error messages
|
||||
textError: 'error06',
|
||||
|
||||
@@ -149,6 +157,9 @@ export const colorMap = {
|
||||
// Background for high-contrast input fields
|
||||
field02: 'surface11',
|
||||
|
||||
// Color for the section divider
|
||||
dividerColor: 'surface12',
|
||||
|
||||
// Background for high-contrast input fields on hover
|
||||
field02Hover: 'primary09',
|
||||
|
||||
|
||||
@@ -120,6 +120,13 @@ function ConferenceInfo(props: Props) {
|
||||
}
|
||||
{ _showParticipantCount && <ParticipantsCount /> }
|
||||
<E2EELabel />
|
||||
{_hideRecordingLabel && (
|
||||
<>
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
<LocalRecordingLabel />
|
||||
</>
|
||||
)}
|
||||
<TranscribingLabel />
|
||||
<VideoQualityLabel />
|
||||
<InsecureRoomNameLabel />
|
||||
@@ -148,10 +155,12 @@ function _mapStateToProps(state) {
|
||||
hideConferenceTimer,
|
||||
hideConferenceSubject,
|
||||
hideParticipantsStats,
|
||||
hideRecordingLabel
|
||||
hideRecordingLabel,
|
||||
iAmRecorder
|
||||
} = state['features/base/config'];
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
const shouldHideRecordingLabel = hideRecordingLabel || iAmRecorder;
|
||||
const fileRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.FILE);
|
||||
const streamRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.STREAM);
|
||||
const isFileRecording = fileRecordingStatus ? fileRecordingStatus !== JitsiRecordingConstants.status.OFF : false;
|
||||
@@ -162,12 +171,12 @@ function _mapStateToProps(state) {
|
||||
return {
|
||||
_hideConferenceNameAndTimer: clientWidth < 300,
|
||||
_hideConferenceTimer: Boolean(hideConferenceTimer),
|
||||
_hideRecordingLabel: hideRecordingLabel,
|
||||
_hideRecordingLabel: shouldHideRecordingLabel,
|
||||
_fullWidth: state['features/video-layout'].tileViewEnabled,
|
||||
_showParticipantCount: participantCount > 2 && !hideParticipantsStats,
|
||||
_subject: hideConferenceSubject ? '' : getConferenceName(state),
|
||||
_visible: isToolboxVisible(state),
|
||||
_recordingLabel: (isFileRecording || isStreamRecording || isEngaged) && !hideRecordingLabel
|
||||
_recordingLabel: (isFileRecording || isStreamRecording || isEngaged) && !shouldHideRecordingLabel
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,9 @@
|
||||
import { getName } from '../app/functions.web';
|
||||
import { isSuboptimalBrowser } from '../base/environment';
|
||||
import { translateToHTML } from '../base/i18n';
|
||||
import { getLocalParticipant } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
import { getBackendSafePath, getJitsiMeetGlobalNS } from '../base/util';
|
||||
import { showWarningNotification } from '../notifications';
|
||||
import { createRnnoiseProcessor } from '../stream-effects/rnnoise';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Returns the result of getWiFiStats from the global NS or does nothing
|
||||
(returns empty result).
|
||||
* Fixes a concurrency problem where we need to pass a function when creating
|
||||
* a JitsiConference, but that method is added to the context later.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
const getWiFiStatsMethod = () => {
|
||||
const gloabalNS = getJitsiMeetGlobalNS();
|
||||
|
||||
return gloabalNS.getWiFiStats ? gloabalNS.getWiFiStats() : Promise.resolve('{}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the suboptimal experience notification if needed.
|
||||
*
|
||||
@@ -49,48 +29,3 @@ export function maybeShowSuboptimalExperienceNotification(dispatch, t) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object aggregating the conference options.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux store state.
|
||||
* @returns {Object} - Options object.
|
||||
*/
|
||||
export function getConferenceOptions(stateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
const options = state['features/base/config'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const { tenant } = state['features/base/jwt'];
|
||||
|
||||
const { email, name: nick } = getLocalParticipant(state);
|
||||
|
||||
if (tenant) {
|
||||
options.siteID = tenant;
|
||||
}
|
||||
|
||||
if (options.enableDisplayNameInStats && nick) {
|
||||
options.statisticsDisplayName = nick;
|
||||
}
|
||||
|
||||
if (options.enableEmailInStats && email) {
|
||||
options.statisticsId = email;
|
||||
}
|
||||
|
||||
if (locationURL) {
|
||||
options.confID = `${locationURL.host}${getBackendSafePath(locationURL.pathname)}`;
|
||||
}
|
||||
|
||||
options.applicationName = getName();
|
||||
options.getWiFiStatsMethod = getWiFiStatsMethod;
|
||||
options.createVADProcessor = createRnnoiseProcessor;
|
||||
|
||||
// Disable CallStats, if requessted.
|
||||
if (options.disableThirdPartyRequests) {
|
||||
delete options.callStatsID;
|
||||
delete options.callStatsSecret;
|
||||
delete options.getWiFiStatsMethod;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getDeviceIdByLabel,
|
||||
groupDevicesByKind,
|
||||
setAudioInputDeviceAndUpdateSettings,
|
||||
setAudioOutputDeviceId,
|
||||
setAudioOutputDevice,
|
||||
setVideoInputDeviceAndUpdateSettings
|
||||
} from '../base/devices';
|
||||
import { isIosMobileBrowser } from '../base/environment/utils';
|
||||
@@ -189,12 +189,11 @@ export function processExternalDeviceRequest( // eslint-disable-line max-params
|
||||
|
||||
if (deviceId) {
|
||||
switch (device.kind) {
|
||||
case 'audioinput': {
|
||||
case 'audioinput':
|
||||
dispatch(setAudioInputDeviceAndUpdateSettings(deviceId));
|
||||
break;
|
||||
}
|
||||
case 'audiooutput':
|
||||
setAudioOutputDeviceId(deviceId, dispatch);
|
||||
dispatch(setAudioOutputDevice(deviceId));
|
||||
break;
|
||||
case 'videoinput':
|
||||
dispatch(setVideoInputDeviceAndUpdateSettings(deviceId));
|
||||
|
||||
@@ -6,12 +6,15 @@ import { 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 { isFilmstripVisible } from '../../functions';
|
||||
import { isFilmstripVisible, shouldRemoteVideosBeVisible } from '../../functions';
|
||||
|
||||
import LocalThumbnail from './LocalThumbnail';
|
||||
import Thumbnail from './Thumbnail';
|
||||
import styles from './styles';
|
||||
|
||||
// Immutable reference to avoid re-renders.
|
||||
const NO_REMOTE_VIDEOS = [];
|
||||
|
||||
/**
|
||||
* Filmstrip component's property types.
|
||||
*/
|
||||
@@ -167,10 +170,11 @@ class Filmstrip extends Component<Props> {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { enabled, remoteParticipants } = state['features/filmstrip'];
|
||||
const showRemoteVideos = shouldRemoteVideosBeVisible(state);
|
||||
|
||||
return {
|
||||
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
|
||||
_participants: remoteParticipants,
|
||||
_participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS,
|
||||
_visible: enabled && isFilmstripVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
getParticipantCount,
|
||||
isEveryoneModerator,
|
||||
pinParticipant,
|
||||
getParticipantByIdOrUndefined
|
||||
getParticipantByIdOrUndefined,
|
||||
getLocalParticipant
|
||||
} from '../../../base/participants';
|
||||
import { Container } from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
@@ -24,6 +25,8 @@ import { DisplayNameLabel } from '../../../display-name';
|
||||
import { toggleToolboxVisible } from '../../../toolbox/actions.native';
|
||||
import { RemoteVideoMenu } from '../../../video-menu';
|
||||
import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
|
||||
import SharedVideoMenu
|
||||
from '../../../video-menu/components/native/SharedVideoMenu';
|
||||
|
||||
import AudioMutedIndicator from './AudioMutedIndicator';
|
||||
import DominantSpeakerIndicator from './DominantSpeakerIndicator';
|
||||
@@ -48,6 +51,11 @@ type Props = {
|
||||
*/
|
||||
_largeVideo: Object,
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
*/
|
||||
_localVideoOwner: boolean,
|
||||
|
||||
/**
|
||||
* The Redux representation of the participant to display.
|
||||
*/
|
||||
@@ -116,6 +124,7 @@ function Thumbnail(props: Props) {
|
||||
const {
|
||||
_audioMuted: audioMuted,
|
||||
_largeVideo: largeVideo,
|
||||
_localVideoOwner,
|
||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||
_renderModeratorIndicator: renderModeratorIndicator,
|
||||
_participant: participant,
|
||||
@@ -144,6 +153,12 @@ function Thumbnail(props: Props) {
|
||||
dispatch(openDialog(ConnectionStatusComponent, {
|
||||
participantID: participant.id
|
||||
}));
|
||||
} else if (participant.isFakeParticipant) {
|
||||
if (_localVideoOwner) {
|
||||
dispatch(openDialog(SharedVideoMenu, {
|
||||
participant
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
dispatch(openDialog(RemoteVideoMenu, {
|
||||
participant
|
||||
@@ -223,9 +238,11 @@ function _mapStateToProps(state, ownProps) {
|
||||
// filmstrip doesn't render the video of the participant who is rendered on
|
||||
// the stage i.e. as a large video.
|
||||
const largeVideo = state['features/large-video'];
|
||||
const { ownerId } = state['features/shared-video'];
|
||||
const tracks = state['features/base/tracks'];
|
||||
const { participantID } = ownProps;
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
const localParticipantId = getLocalParticipant(state).id;
|
||||
const id = participant?.id;
|
||||
const audioTrack
|
||||
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
|
||||
@@ -240,6 +257,7 @@ function _mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
_audioMuted: audioTrack?.muted ?? true,
|
||||
_largeVideo: largeVideo,
|
||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||
_participant: participant,
|
||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||
_renderModeratorIndicator: renderModeratorIndicator,
|
||||
|
||||
@@ -284,10 +284,10 @@ class Filmstrip extends PureComponent <Props> {
|
||||
* @param {Object} data - Information about the rendered items.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onListItemsRendered({ overscanStartIndex, overscanStopIndex }) {
|
||||
_onListItemsRendered({ visibleStartIndex, visibleStopIndex }) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(setVisibleRemoteParticipants(overscanStartIndex, overscanStopIndex));
|
||||
dispatch(setVisibleRemoteParticipants(visibleStartIndex, visibleStopIndex));
|
||||
}
|
||||
|
||||
_onGridItemsRendered: Object => void;
|
||||
@@ -299,14 +299,14 @@ class Filmstrip extends PureComponent <Props> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onGridItemsRendered({
|
||||
overscanColumnStartIndex,
|
||||
overscanColumnStopIndex,
|
||||
overscanRowStartIndex,
|
||||
overscanRowStopIndex
|
||||
visibleColumnStartIndex,
|
||||
visibleColumnStopIndex,
|
||||
visibleRowStartIndex,
|
||||
visibleRowStopIndex
|
||||
}) {
|
||||
const { _columns, dispatch } = this.props;
|
||||
const startIndex = (overscanRowStartIndex * _columns) + overscanColumnStartIndex;
|
||||
const endIndex = (overscanRowStopIndex * _columns) + overscanColumnStopIndex;
|
||||
const startIndex = (visibleRowStartIndex * _columns) + visibleColumnStartIndex;
|
||||
const endIndex = (visibleRowStopIndex * _columns) + visibleColumnStopIndex;
|
||||
|
||||
dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
|
||||
}
|
||||
@@ -345,6 +345,7 @@ class Filmstrip extends PureComponent <Props> {
|
||||
initialScrollTop = { 0 }
|
||||
itemKey = { this._gridItemKey }
|
||||
onItemsRendered = { this._onGridItemsRendered }
|
||||
overscanRowCount = { 1 }
|
||||
rowCount = { _rows }
|
||||
rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN }
|
||||
width = { _filmstripWidth }>
|
||||
@@ -363,6 +364,7 @@ class Filmstrip extends PureComponent <Props> {
|
||||
itemKey: this._listItemKey,
|
||||
itemSize: 0,
|
||||
onItemsRendered: this._onListItemsRendered,
|
||||
overscanCount: 1,
|
||||
width: _filmstripWidth,
|
||||
style: {
|
||||
willChange: 'auto'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { getParticipantCountWithFake, getPinnedParticipant } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
|
||||
/**
|
||||
@@ -26,3 +26,34 @@ export function isFilmstripVisible(stateful: Object | Function) {
|
||||
return getParticipantCountWithFake(state) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the remote video thumbnails should be displayed/visible in
|
||||
* the filmstrip.
|
||||
*
|
||||
* @param {Object} state - The full redux state.
|
||||
* @returns {boolean} - If remote video thumbnails should be displayed/visible
|
||||
* in the filmstrip, then {@code true}; otherwise, {@code false}.
|
||||
*/
|
||||
export function shouldRemoteVideosBeVisible(state: Object) {
|
||||
if (state['features/invite'].calleeInfoVisible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Include fake participants to derive how many thumbnails are dispalyed,
|
||||
// as it is assumed all participants, including fake, will be displayed
|
||||
// in the filmstrip.
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
const pinnedParticipant = getPinnedParticipant(state);
|
||||
const { disable1On1Mode } = state['features/base/config'];
|
||||
|
||||
return Boolean(
|
||||
participantCount > 2
|
||||
|
||||
// Always show the filmstrip when there is another participant to
|
||||
// show and the local video is pinned. Note we are not taking the
|
||||
// toolbar visibility into account here (unlike web) because
|
||||
// showing / hiding views in quick succession on mobile is taxing.
|
||||
|| (participantCount > 1 && pinnedParticipant?.local)
|
||||
|
||||
|| disable1On1Mode);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -98,7 +98,7 @@ function _appWillMount({ dispatch, getState }) {
|
||||
|
||||
switch (command) {
|
||||
case CMD_HANG_UP:
|
||||
if (typeof getCurrentConferenceUrl(getState()) !== undefined) {
|
||||
if (typeof getCurrentConferenceUrl(getState()) !== 'undefined') {
|
||||
dispatch(appNavigate(undefined));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import { openDialog } from '../base/dialog';
|
||||
import { SharedVideoMenu } from '../video-menu';
|
||||
import ConnectionStatusComponent
|
||||
from '../video-menu/components/native/ConnectionStatusComponent';
|
||||
import RemoteVideoMenu from '../video-menu/components/native/RemoteVideoMenu';
|
||||
|
||||
import { SET_VOLUME } from './actionTypes';
|
||||
import {
|
||||
ContextMenuMeetingParticipantDetails,
|
||||
ContextMenuLobbyParticipantReject
|
||||
} from './components/native';
|
||||
export * from './actions.any';
|
||||
@@ -21,13 +24,33 @@ export function showContextMenuReject(participant: Object) {
|
||||
|
||||
|
||||
/**
|
||||
* Displays the context menu for the selected meeting participant.
|
||||
* Displays the connection status for the local meeting participant.
|
||||
*
|
||||
* @param {string} participantID - The selected meeting participant id.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showContextMenuDetails(participantID: String) {
|
||||
return openDialog(ContextMenuMeetingParticipantDetails, { participantID });
|
||||
export function showConnectionStatus(participantID: String) {
|
||||
return openDialog(ConnectionStatusComponent, { participantID });
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the context menu for the selected meeting participant.
|
||||
*
|
||||
* @param {Object} participant - The selected meeting participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showContextMenuDetails(participant: Object) {
|
||||
return openDialog(RemoteVideoMenu, { participant });
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the shared video menu.
|
||||
*
|
||||
* @param {Object} participant - The selected meeting participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showSharedVideoMenu(participant: Object) {
|
||||
return openDialog(SharedVideoMenu, { participant });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TouchableOpacity, View } from 'react-native';
|
||||
import { Divider, Text } from 'react-native-paper';
|
||||
import { Text } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
@@ -33,30 +33,33 @@ const ContextMenuLobbyParticipantReject = ({ participant: p }: Props) => {
|
||||
const reject = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, false), [ dispatch ]));
|
||||
const { t } = useTranslation();
|
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const renderMenuHeader = () => (
|
||||
<View
|
||||
style = { styles.contextMenuItemSectionAvatar }>
|
||||
<Avatar
|
||||
className = 'participant-avatar'
|
||||
participantId = { p.id }
|
||||
size = { 24 } />
|
||||
<Text style = { styles.contextMenuItemName }>
|
||||
{ displayName }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
addScrollViewPadding = { false }
|
||||
onCancel = { cancel }
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
renderHeader = { renderMenuHeader }
|
||||
showSlidingView = { Boolean(knockParticipantIsAvailable) }
|
||||
style = { styles.contextMenuMore }>
|
||||
<View
|
||||
style = { styles.contextMenuItemSectionAvatar }>
|
||||
<Avatar
|
||||
className = 'participant-avatar'
|
||||
participantId = { p.id }
|
||||
size = { 20 } />
|
||||
<View style = { styles.contextMenuItemAvatarText }>
|
||||
<Text style = { styles.contextMenuItemName }>
|
||||
{ displayName }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Divider style = { styles.divider } />
|
||||
<TouchableOpacity
|
||||
onPress = { reject }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
size = { 24 }
|
||||
src = { IconClose } />
|
||||
<Text style = { styles.contextMenuItemText }>{ t('lobby.reject') }</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TouchableOpacity, View } from 'react-native';
|
||||
import { Divider, Text } from 'react-native-paper';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { hideDialog, openDialog } from '../../../base/dialog/actions';
|
||||
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
|
||||
import {
|
||||
Icon, IconCloseCircle, IconMessage,
|
||||
IconMicrophoneEmptySlash,
|
||||
IconMuteEveryoneElse, IconVideoOff
|
||||
} from '../../../base/icons';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantByIdOrUndefined,
|
||||
getParticipantDisplayName, getRemoteParticipants,
|
||||
isLocalParticipantModerator
|
||||
} from '../../../base/participants/functions';
|
||||
import { connect } from '../../../base/redux';
|
||||
import {
|
||||
isParticipantAudioMuted,
|
||||
isParticipantVideoMuted
|
||||
} from '../../../base/tracks/functions';
|
||||
import { openChat } from '../../../chat/actions.native';
|
||||
import {
|
||||
KickRemoteParticipantDialog,
|
||||
MuteEveryoneDialog,
|
||||
MuteRemoteParticipantDialog,
|
||||
MuteRemoteParticipantsVideoDialog
|
||||
} from '../../../video-menu';
|
||||
import VolumeSlider from '../../../video-menu/components/native/VolumeSlider';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The display name of the participant.
|
||||
*/
|
||||
_displayName: string,
|
||||
|
||||
/**
|
||||
* True if the local participant is moderator and false otherwise.
|
||||
*/
|
||||
_isLocalModerator: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is moderator and false otherwise.
|
||||
*/
|
||||
_isParticipantModerator: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is video muted and false otherwise.
|
||||
*/
|
||||
_isParticipantVideoMuted: boolean,
|
||||
|
||||
/**
|
||||
* True if the participant is audio muted and false otherwise.
|
||||
*/
|
||||
_isParticipantAudioMuted: boolean,
|
||||
|
||||
/**
|
||||
* Whether the participant is present in the room or not.
|
||||
*/
|
||||
_isParticipantIDAvailable?: boolean,
|
||||
|
||||
/**
|
||||
* Participant reference
|
||||
*/
|
||||
_participant: Object,
|
||||
|
||||
/**
|
||||
* The ID of the participant.
|
||||
*/
|
||||
participantID: string,
|
||||
};
|
||||
|
||||
const ContextMenuMeetingParticipantDetails = (
|
||||
{
|
||||
_displayName,
|
||||
_isLocalModerator,
|
||||
_isParticipantVideoMuted,
|
||||
_isParticipantAudioMuted,
|
||||
_participant,
|
||||
_isParticipantIDAvailable,
|
||||
participantID
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
|
||||
const kickRemoteParticipant = useCallback(() => {
|
||||
dispatch(openDialog(KickRemoteParticipantDialog, {
|
||||
participantID
|
||||
}));
|
||||
}, [ dispatch, participantID ]);
|
||||
const muteAudio = useCallback(() => {
|
||||
dispatch(openDialog(MuteRemoteParticipantDialog, {
|
||||
participantID
|
||||
}));
|
||||
}, [ dispatch, participantID ]);
|
||||
const muteEveryoneElse = useCallback(() => {
|
||||
dispatch(openDialog(MuteEveryoneDialog, {
|
||||
exclude: [ participantID ]
|
||||
}));
|
||||
}, [ dispatch, participantID ]);
|
||||
const muteVideo = useCallback(() => {
|
||||
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
|
||||
participantID
|
||||
}));
|
||||
}, [ dispatch, participantID ]);
|
||||
|
||||
const sendPrivateMessage = useCallback(() => {
|
||||
dispatch(hideDialog());
|
||||
dispatch(openChat(_participant));
|
||||
}, [ dispatch, _participant ]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
addScrollViewPadding = { false }
|
||||
onCancel = { cancel }
|
||||
showSlidingView = { _isParticipantIDAvailable }
|
||||
style = { styles.contextMenuMeetingParticipantDetails }>
|
||||
<View
|
||||
style = { styles.contextMenuItemSectionAvatar }>
|
||||
<Avatar
|
||||
className = 'participant-avatar'
|
||||
participantId = { participantID }
|
||||
size = { 20 } />
|
||||
<View style = { styles.contextMenuItemAvatarText }>
|
||||
<Text style = { styles.contextMenuItemName }>
|
||||
{ _displayName }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Divider style = { styles.divider } />
|
||||
{
|
||||
_isLocalModerator && (
|
||||
<>
|
||||
{
|
||||
!_isParticipantAudioMuted
|
||||
&& <TouchableOpacity
|
||||
onPress = { muteAudio }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
src = { IconMicrophoneEmptySlash } />
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{ t('participantsPane.actions.mute') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
|
||||
<TouchableOpacity
|
||||
onPress = { muteEveryoneElse }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
src = { IconMuteEveryoneElse } />
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{ t('participantsPane.actions.muteEveryoneElse') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Divider style = { styles.divider } />
|
||||
{
|
||||
_isLocalModerator && (
|
||||
<>
|
||||
{
|
||||
!_isParticipantVideoMuted
|
||||
&& <TouchableOpacity
|
||||
onPress = { muteVideo }
|
||||
style = { styles.contextMenuItemSection }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
src = { IconVideoOff } />
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{ t('participantsPane.actions.stopVideo') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
|
||||
<TouchableOpacity
|
||||
onPress = { kickRemoteParticipant }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
src = { IconCloseCircle } />
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{ t('videothumbnail.kick') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<TouchableOpacity
|
||||
onPress = { sendPrivateMessage }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
src = { IconMessage } />
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{ t('toolbar.accessibilityLabel.privateMessage') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{/* We need design specs for this*/}
|
||||
{/* <TouchableOpacity*/}
|
||||
{/* style = { styles.contextMenuItemSection }>*/}
|
||||
{/* <Icon*/}
|
||||
{/* size = { 20 }*/}
|
||||
{/* src = { IconConnectionActive }*/}
|
||||
{/* style = { styles.contextMenuItemIcon } />*/}
|
||||
{/* <Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.networkStats') }</Text>*/}
|
||||
{/* </TouchableOpacity>*/}
|
||||
<Divider style = { styles.divider } />
|
||||
<VolumeSlider participantID = { participantID } />
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps): Object {
|
||||
const { participantID } = ownProps;
|
||||
const participantIDS = [];
|
||||
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
const _isLocalModerator = isLocalParticipantModerator(state);
|
||||
const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state);
|
||||
const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const remoteParticipants = getRemoteParticipants(state);
|
||||
|
||||
localParticipant && participantIDS.push(localParticipant?.id);
|
||||
|
||||
remoteParticipants.forEach(p => {
|
||||
participantIDS.push(p?.id);
|
||||
});
|
||||
|
||||
const isParticipantIDAvailable = participantIDS.find(partID => partID === participantID);
|
||||
|
||||
return {
|
||||
_displayName: getParticipantDisplayName(state, participantID),
|
||||
_isLocalModerator,
|
||||
_isParticipantAudioMuted,
|
||||
_isParticipantIDAvailable: Boolean(isParticipantIDAvailable),
|
||||
_isParticipantVideoMuted,
|
||||
_participant: participant
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(ContextMenuMeetingParticipantDetails);
|
||||
@@ -9,14 +9,13 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { openDialog, hideDialog } from '../../../base/dialog/actions';
|
||||
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
|
||||
import {
|
||||
Icon, IconMicDisabledHollow,
|
||||
Icon,
|
||||
IconVideoOff
|
||||
} from '../../../base/icons';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCount
|
||||
} from '../../../base/participants';
|
||||
import { BlockAudioVideoDialog } from '../../../video-menu';
|
||||
import MuteEveryonesVideoDialog
|
||||
from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
|
||||
|
||||
@@ -24,7 +23,6 @@ import styles from './styles';
|
||||
|
||||
export const ContextMenuMore = () => {
|
||||
const dispatch = useDispatch();
|
||||
const blockAudioVideo = useCallback(() => dispatch(openDialog(BlockAudioVideoDialog)), [ dispatch ]);
|
||||
const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
|
||||
const { id } = useSelector(getLocalParticipant);
|
||||
const participantsCount = useSelector(getParticipantCount);
|
||||
@@ -45,21 +43,10 @@ export const ContextMenuMore = () => {
|
||||
onPress = { muteAllVideo }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
size = { 24 }
|
||||
src = { IconVideoOff } />
|
||||
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress = { blockAudioVideo }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
src = { IconMicDisabledHollow }
|
||||
style = { styles.contextMenuIcon } />
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{t('participantsPane.actions.blockEveryoneMicCamera')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Text, View } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { Button, withTheme } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { admitMultiple } from '../../../lobby/actions.native';
|
||||
@@ -12,17 +12,25 @@ import { getLobbyState } from '../../../lobby/functions';
|
||||
import { LobbyParticipantItem } from './LobbyParticipantItem';
|
||||
import styles from './styles';
|
||||
|
||||
export const LobbyParticipantList = () => {
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Theme used for styles.
|
||||
*/
|
||||
theme: Object
|
||||
};
|
||||
|
||||
const LobbyParticipantList = ({ theme }: Props) => {
|
||||
const {
|
||||
lobbyEnabled,
|
||||
knockingParticipants: participants
|
||||
} = useSelector(getLobbyState);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const admitAll = useCallback(() =>
|
||||
dispatch(admitMultiple(participants)),
|
||||
[ dispatch ]);
|
||||
[ dispatch, participants ]);
|
||||
const { t } = useTranslation();
|
||||
const { palette } = theme;
|
||||
|
||||
if (!lobbyEnabled || !participants.length) {
|
||||
return null;
|
||||
@@ -35,14 +43,17 @@ export const LobbyParticipantList = () => {
|
||||
{t('participantsPane.headings.waitingLobby',
|
||||
{ count: participants.length })}
|
||||
</Text>
|
||||
<Button
|
||||
color = '#3D3D3D'
|
||||
labelStyle = { styles.admitAllParticipantsActionButtonLabel }
|
||||
mode = 'text'
|
||||
onPress = { admitAll }
|
||||
style = { styles.admitAllParticipantsActionButton }>
|
||||
{t('lobby.admitAll')}
|
||||
</Button>
|
||||
{
|
||||
participants.length > 1 && (
|
||||
<Button
|
||||
color = { palette.action02 }
|
||||
labelStyle = { styles.admitAllParticipantsActionButtonLabel }
|
||||
mode = 'text'
|
||||
onPress = { admitAll }>
|
||||
{t('lobby.admitAll')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
{
|
||||
participants.map(p => (
|
||||
@@ -54,3 +65,5 @@ export const LobbyParticipantList = () => {
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTheme(LobbyParticipantList);
|
||||
|
||||
@@ -6,20 +6,34 @@ import { Text, View } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
|
||||
import { Icon, IconInviteMore } from '../../../base/icons';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCountWithFake,
|
||||
getRemoteParticipants
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { doInvitePeople } from '../../../invite/actions.native';
|
||||
import { showContextMenuDetails } from '../../actions.native';
|
||||
import {
|
||||
showConnectionStatus,
|
||||
showContextMenuDetails,
|
||||
showSharedVideoMenu
|
||||
} from '../../actions.native';
|
||||
import { shouldRenderInviteButton } from '../../functions';
|
||||
|
||||
import MeetingParticipantItem from './MeetingParticipantItem';
|
||||
import styles from './styles';
|
||||
|
||||
export const MeetingParticipantList = () => {
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
*/
|
||||
_localVideoOwner: boolean
|
||||
}
|
||||
|
||||
const MeetingParticipantList = ({ _localVideoOwner }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const items = [];
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
@@ -30,18 +44,39 @@ export const MeetingParticipantList = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const renderParticipant = id => (
|
||||
<MeetingParticipantItem
|
||||
key = { id }
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
onPress = { () => dispatch(showContextMenuDetails(id)) }
|
||||
participantID = { id } />
|
||||
);
|
||||
const renderParticipant = p => {
|
||||
if (p.isFakeParticipant) {
|
||||
if (_localVideoOwner) {
|
||||
return (
|
||||
<MeetingParticipantItem
|
||||
key = { p.id }
|
||||
/* eslint-disable-next-line react/jsx-no-bind,no-confusing-arrow */
|
||||
onPress = { () => dispatch(showSharedVideoMenu(p)) }
|
||||
participantID = { p.id } />
|
||||
);
|
||||
}
|
||||
|
||||
localParticipant && items.push(renderParticipant(localParticipant?.id));
|
||||
return (
|
||||
<MeetingParticipantItem
|
||||
key = { p.id }
|
||||
participantID = { p.id } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MeetingParticipantItem
|
||||
key = { p.id }
|
||||
/* eslint-disable-next-line react/jsx-no-bind,no-confusing-arrow */
|
||||
onPress = { () => p.local
|
||||
? dispatch(showConnectionStatus(p.id)) : dispatch(showContextMenuDetails(p)) }
|
||||
participantID = { p.id } />
|
||||
);
|
||||
};
|
||||
|
||||
items.push(renderParticipant(localParticipant));
|
||||
|
||||
participants.forEach(p => {
|
||||
items.push(renderParticipant(p?.id));
|
||||
items.push(renderParticipant(p));
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -70,3 +105,20 @@ export const MeetingParticipantList = () => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state): Object {
|
||||
const { ownerId } = state['features/shared-video'];
|
||||
const localParticipantId = getLocalParticipant(state).id;
|
||||
|
||||
return {
|
||||
_localVideoOwner: Boolean(ownerId === localParticipantId)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(MeetingParticipantList);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { openDialog } from '../../../base/dialog';
|
||||
import { Icon, IconClose, IconHorizontalPoints } from '../../../base/icons';
|
||||
import { Icon, IconHorizontalPoints } from '../../../base/icons';
|
||||
import { JitsiModal } from '../../../base/modal';
|
||||
import {
|
||||
getParticipantCount,
|
||||
@@ -18,8 +18,8 @@ import MuteEveryoneDialog
|
||||
import { close } from '../../actions.native';
|
||||
|
||||
import { ContextMenuMore } from './ContextMenuMore';
|
||||
import { LobbyParticipantList } from './LobbyParticipantList';
|
||||
import { MeetingParticipantList } from './MeetingParticipantList';
|
||||
import LobbyParticipantList from './LobbyParticipantList';
|
||||
import MeetingParticipantList from './MeetingParticipantList';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
@@ -40,21 +40,11 @@ const ParticipantsPane = () => {
|
||||
|
||||
return (
|
||||
<JitsiModal
|
||||
hideHeaderWithNavigation = { true }
|
||||
headerProps = {{
|
||||
headerLabelKey: 'participantsPane.header'
|
||||
}}
|
||||
onClose = { closePane }
|
||||
style = { styles.participantsPane }>
|
||||
<View style = { styles.header }>
|
||||
<Button
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
icon = { () =>
|
||||
(<Icon
|
||||
size = { 20 }
|
||||
src = { IconClose } />)
|
||||
}
|
||||
labelStyle = { styles.closeIcon }
|
||||
mode = 'contained'
|
||||
onPress = { closePane }
|
||||
style = { styles.closeButton } />
|
||||
</View>
|
||||
<ScrollView>
|
||||
<LobbyParticipantList />
|
||||
<MeetingParticipantList />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
export { default as MeetingParticipantList } from './MeetingParticipantList';
|
||||
export { default as ParticipantsPane } from './ParticipantsPane';
|
||||
export { default as ParticipantsPaneButton } from './ParticipantsPaneButton';
|
||||
export { default as ContextMenuLobbyParticipantReject } from './ContextMenuLobbyParticipantReject';
|
||||
export { default as ContextMenuMeetingParticipantDetails } from './ContextMenuMeetingParticipantDetails';
|
||||
|
||||
@@ -116,10 +116,6 @@ export default {
|
||||
textTransform: 'capitalize'
|
||||
},
|
||||
|
||||
admitAllParticipantsActionButton: {
|
||||
marginLeft: 'auto'
|
||||
},
|
||||
|
||||
participantContainer: {
|
||||
alignItems: 'center',
|
||||
borderBottomColor: BaseTheme.palette.field01Hover,
|
||||
@@ -145,7 +141,7 @@ export default {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingLeft: BaseTheme.spacing[2],
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
width: '63%'
|
||||
},
|
||||
|
||||
@@ -187,6 +183,7 @@ export default {
|
||||
...flexContent,
|
||||
top: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
lobbyList: {
|
||||
position: 'relative'
|
||||
},
|
||||
@@ -216,19 +213,6 @@ export default {
|
||||
marginLeft: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
top: BaseTheme.spacing[0],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: BaseTheme.spacing[10],
|
||||
paddingRight: BaseTheme.spacing[3],
|
||||
position: 'relative',
|
||||
right: BaseTheme.spacing[0],
|
||||
left: BaseTheme.spacing[0]
|
||||
},
|
||||
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
@@ -243,17 +227,6 @@ export default {
|
||||
left: BaseTheme.spacing[0]
|
||||
},
|
||||
|
||||
closeButton: {
|
||||
...smallButton,
|
||||
marginLeft: 'auto'
|
||||
},
|
||||
|
||||
closeIcon: {
|
||||
...buttonContent,
|
||||
height: BaseTheme.spacing[5],
|
||||
marginLeft: 'auto'
|
||||
},
|
||||
|
||||
inviteButton: {
|
||||
backgroundColor: BaseTheme.palette.action01,
|
||||
marginTop: BaseTheme.spacing[2],
|
||||
@@ -277,12 +250,7 @@ export default {
|
||||
},
|
||||
|
||||
contextMenuMore: {
|
||||
backgroundColor: BaseTheme.palette.action02,
|
||||
borderRadius: BaseTheme.shape.borderRadius
|
||||
},
|
||||
|
||||
contextMenuMeetingParticipantDetails: {
|
||||
backgroundColor: BaseTheme.palette.action02,
|
||||
backgroundColor: BaseTheme.palette.bottomSheet,
|
||||
borderRadius: BaseTheme.shape.borderRadius
|
||||
},
|
||||
|
||||
@@ -303,8 +271,6 @@ export default {
|
||||
marginHorizontal: BaseTheme.spacing[0],
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
paddingRight: BaseTheme.spacing[3],
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
textTransform: 'capitalize',
|
||||
width: 94
|
||||
},
|
||||
@@ -318,13 +284,15 @@ export default {
|
||||
},
|
||||
|
||||
contextMenuItemSectionAvatar: {
|
||||
...contextMenuItem,
|
||||
marginLeft: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
contextMenuItemAvatarText: {
|
||||
...contextMenuItemText,
|
||||
marginLeft: BaseTheme.spacing[3]
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.bottomSheet,
|
||||
borderBottomColor: BaseTheme.palette.dividerColor,
|
||||
borderBottomWidth: 1,
|
||||
borderTopLeftRadius: BaseTheme.spacing[3],
|
||||
borderTopRightRadius: BaseTheme.spacing[3],
|
||||
flexDirection: 'row',
|
||||
height: BaseTheme.spacing[7],
|
||||
paddingLeft: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
contextMenuItemText: {
|
||||
@@ -333,15 +301,14 @@ export default {
|
||||
},
|
||||
|
||||
contextMenuItemName: {
|
||||
...BaseTheme.typography.bodyShortRegularLarge,
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
contextMenuIcon: {
|
||||
color: BaseTheme.palette.actionDanger
|
||||
color: BaseTheme.palette.text04,
|
||||
flexShrink: 1,
|
||||
fontSize: BaseTheme.spacing[3],
|
||||
marginLeft: BaseTheme.spacing[3],
|
||||
opacity: 0.90
|
||||
},
|
||||
|
||||
divider: {
|
||||
backgroundColor: BaseTheme.palette.section01
|
||||
backgroundColor: BaseTheme.palette.dividerColor
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,9 +53,13 @@ export const LobbyParticipantList = () => {
|
||||
<div className = { classes.heading }>
|
||||
{t('participantsPane.headings.lobby', { count: participants.length })}
|
||||
</div>
|
||||
<div
|
||||
className = { classes.link }
|
||||
onClick = { admitAll }>{t('lobby.admitAll')}</div>
|
||||
{
|
||||
participants.length > 1 && (
|
||||
<div
|
||||
className = { classes.link }
|
||||
onClick = { admitAll }>{t('lobby.admitAll')}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
{participants.map(p => (
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
IconMessage,
|
||||
IconMicDisabled,
|
||||
IconMuteEveryoneElse,
|
||||
IconShareVideo,
|
||||
IconVideoOff
|
||||
} from '../../../base/icons';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantByIdOrUndefined,
|
||||
isLocalParticipantModerator,
|
||||
isParticipantModerator
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
import { connect } from '../../../base/redux';
|
||||
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
|
||||
import { openChat } from '../../../chat/actions';
|
||||
import { stopSharedVideo } from '../../../shared-video/actions.any';
|
||||
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
|
||||
import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
|
||||
import { getComputedOuterHeight } from '../../functions';
|
||||
@@ -60,6 +63,11 @@ type Props = {
|
||||
*/
|
||||
_isParticipantAudioMuted: boolean,
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
*/
|
||||
_localVideoOwner: boolean,
|
||||
|
||||
/**
|
||||
* Participant reference
|
||||
*/
|
||||
@@ -143,6 +151,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||
this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
|
||||
this._onMuteVideo = this._onMuteVideo.bind(this);
|
||||
this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
|
||||
this._onStopSharedVideo = this._onStopSharedVideo.bind(this);
|
||||
this._position = this._position.bind(this);
|
||||
}
|
||||
|
||||
@@ -176,6 +185,19 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||
}));
|
||||
}
|
||||
|
||||
_onStopSharedVideo: () => void;
|
||||
|
||||
/**
|
||||
* Stops shared video.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStopSharedVideo() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(stopSharedVideo());
|
||||
}
|
||||
|
||||
_onMuteEveryoneElse: () => void;
|
||||
|
||||
/**
|
||||
@@ -282,6 +304,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||
_isParticipantModerator,
|
||||
_isParticipantVideoMuted,
|
||||
_isParticipantAudioMuted,
|
||||
_localVideoOwner,
|
||||
_participant,
|
||||
onEnter,
|
||||
onLeave,
|
||||
@@ -302,66 +325,81 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||
onClick = { onSelect }
|
||||
onMouseEnter = { onEnter }
|
||||
onMouseLeave = { onLeave }>
|
||||
<ContextMenuItemGroup>
|
||||
{
|
||||
_isLocalModerator && (
|
||||
<>
|
||||
{
|
||||
!_participant.isFakeParticipant && (
|
||||
<>
|
||||
<ContextMenuItemGroup>
|
||||
{
|
||||
!_isParticipantAudioMuted
|
||||
&& <ContextMenuItem onClick = { muteAudio(_participant) }>
|
||||
<ContextMenuIcon src = { IconMicDisabled } />
|
||||
<span>{t('dialog.muteParticipantButton')}</span>
|
||||
</ContextMenuItem>
|
||||
_isLocalModerator && (
|
||||
<>
|
||||
{
|
||||
!_isParticipantAudioMuted
|
||||
&& <ContextMenuItem onClick = { muteAudio(_participant) }>
|
||||
<ContextMenuIcon src = { IconMicDisabled } />
|
||||
<span>{t('dialog.muteParticipantButton')}</span>
|
||||
</ContextMenuItem>
|
||||
}
|
||||
|
||||
<ContextMenuItem onClick = { this._onMuteEveryoneElse }>
|
||||
<ContextMenuIcon src = { IconMuteEveryoneElse } />
|
||||
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<ContextMenuItem onClick = { this._onMuteEveryoneElse }>
|
||||
<ContextMenuIcon src = { IconMuteEveryoneElse } />
|
||||
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
_isLocalModerator && (
|
||||
_isParticipantVideoMuted || (
|
||||
<ContextMenuItem onClick = { this._onMuteVideo }>
|
||||
<ContextMenuIcon src = { IconVideoOff } />
|
||||
<span>{t('participantsPane.actions.stopVideo')}</span>
|
||||
</ContextMenuItem>
|
||||
)
|
||||
)
|
||||
}
|
||||
</ContextMenuItemGroup>
|
||||
|
||||
<ContextMenuItemGroup>
|
||||
{
|
||||
_isLocalModerator && (
|
||||
<>
|
||||
{
|
||||
!_isParticipantModerator && (
|
||||
<ContextMenuItem onClick = { this._onGrantModerator }>
|
||||
<ContextMenuIcon src = { IconCrown } />
|
||||
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
|
||||
_isLocalModerator && (
|
||||
_isParticipantVideoMuted || (
|
||||
<ContextMenuItem onClick = { this._onMuteVideo }>
|
||||
<ContextMenuIcon src = { IconVideoOff } />
|
||||
<span>{t('participantsPane.actions.stopVideo')}</span>
|
||||
</ContextMenuItem>
|
||||
)
|
||||
)
|
||||
}
|
||||
</ContextMenuItemGroup>
|
||||
|
||||
<ContextMenuItemGroup>
|
||||
{
|
||||
_isLocalModerator && (
|
||||
<>
|
||||
{
|
||||
!_isParticipantModerator && (
|
||||
<ContextMenuItem onClick = { this._onGrantModerator }>
|
||||
<ContextMenuIcon src = { IconCrown } />
|
||||
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
<ContextMenuItem onClick = { this._onKick }>
|
||||
<ContextMenuIcon src = { IconCloseCircle } />
|
||||
<span>{ t('videothumbnail.kick') }</span>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
_isChatButtonEnabled && (
|
||||
<ContextMenuItem onClick = { this._onSendPrivateMessage }>
|
||||
<ContextMenuIcon src = { IconMessage } />
|
||||
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
<ContextMenuItem onClick = { this._onKick }>
|
||||
<ContextMenuIcon src = { IconCloseCircle } />
|
||||
<span>{ t('videothumbnail.kick') }</span>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
_isChatButtonEnabled && (
|
||||
<ContextMenuItem onClick = { this._onSendPrivateMessage }>
|
||||
<ContextMenuIcon src = { IconMessage } />
|
||||
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
</ContextMenuItemGroup>
|
||||
</ContextMenuItemGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
_participant.isFakeParticipant && _localVideoOwner && (
|
||||
<ContextMenuItem onClick = { this._onStopSharedVideo }>
|
||||
<ContextMenuIcon src = { IconShareVideo } />
|
||||
<span>{t('toolbar.stopSharedVideo')}</span>
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
@@ -377,7 +415,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps): Object {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
const { ownerId } = state['features/shared-video'];
|
||||
const localParticipantId = getLocalParticipant(state).id;
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
|
||||
const _isLocalModerator = isLocalParticipantModerator(state);
|
||||
@@ -392,6 +431,7 @@ function _mapStateToProps(state, ownProps): Object {
|
||||
_isParticipantModerator,
|
||||
_isParticipantVideoMuted,
|
||||
_isParticipantAudioMuted,
|
||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||
_participant: participant
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getParticipantByIdOrUndefined, getParticipantDisplayName } from '../../../base/participants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantByIdOrUndefined,
|
||||
getParticipantDisplayName
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
|
||||
import { ACTION_TRIGGER, MEDIA_STATE, type MediaState } from '../../constants';
|
||||
@@ -34,6 +38,16 @@ type Props = {
|
||||
*/
|
||||
_local: boolean,
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
*/
|
||||
_localVideoOwner: boolean,
|
||||
|
||||
/**
|
||||
* The participant.
|
||||
*/
|
||||
_participant: Object,
|
||||
|
||||
/**
|
||||
* The participant ID.
|
||||
*
|
||||
@@ -108,7 +122,9 @@ function MeetingParticipantItem({
|
||||
_audioMediaState,
|
||||
_displayName,
|
||||
_isVideoMuted,
|
||||
_localVideoOwner,
|
||||
_local,
|
||||
_participant,
|
||||
_participantID,
|
||||
_quickActionButtonType,
|
||||
_raisedHand,
|
||||
@@ -133,15 +149,28 @@ function MeetingParticipantItem({
|
||||
raisedHand = { _raisedHand }
|
||||
videoMuteState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }
|
||||
youText = { youText }>
|
||||
<ParticipantQuickAction
|
||||
askUnmuteText = { askUnmuteText }
|
||||
buttonType = { _quickActionButtonType }
|
||||
muteAudio = { muteAudio }
|
||||
muteParticipantButtonText = { muteParticipantButtonText }
|
||||
participantID = { _participantID } />
|
||||
<ParticipantActionEllipsis
|
||||
aria-label = { participantActionEllipsisLabel }
|
||||
onClick = { onContextMenu } />
|
||||
{
|
||||
!_participant.isFakeParticipant && (
|
||||
<>
|
||||
<ParticipantQuickAction
|
||||
askUnmuteText = { askUnmuteText }
|
||||
buttonType = { _quickActionButtonType }
|
||||
muteAudio = { muteAudio }
|
||||
muteParticipantButtonText = { muteParticipantButtonText }
|
||||
participantID = { _participantID } />
|
||||
<ParticipantActionEllipsis
|
||||
aria-label = { participantActionEllipsisLabel }
|
||||
onClick = { onContextMenu } />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
_participant.isFakeParticipant && _localVideoOwner && (
|
||||
<ParticipantActionEllipsis
|
||||
aria-label = { participantActionEllipsisLabel }
|
||||
onClick = { onContextMenu } />
|
||||
)
|
||||
}
|
||||
</ParticipantItem>
|
||||
);
|
||||
}
|
||||
@@ -156,6 +185,8 @@ function MeetingParticipantItem({
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps): Object {
|
||||
const { participantID } = ownProps;
|
||||
const { ownerId } = state['features/shared-video'];
|
||||
const localParticipantId = getLocalParticipant(state).id;
|
||||
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
|
||||
@@ -170,6 +201,8 @@ function _mapStateToProps(state, ownProps): Object {
|
||||
_isAudioMuted,
|
||||
_isVideoMuted,
|
||||
_local: Boolean(participant?.local),
|
||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||
_participant: participant,
|
||||
_participantID: participant?.id,
|
||||
_quickActionButtonType,
|
||||
_raisedHand: Boolean(participant?.raisedHand)
|
||||
|
||||
@@ -235,7 +235,6 @@ export const ParticipantActionsHover = styled(ParticipantActions)`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateX(-100%);
|
||||
width: 40px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import React from 'react';
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { BaseApp } from '../../../features/base/app';
|
||||
import { getConferenceOptions } from '../../base/conference/functions';
|
||||
import { setConfig } from '../../base/config';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import { createPrejoinTracks } from '../../base/tracks';
|
||||
import { getConferenceOptions } from '../../conference/functions';
|
||||
import { initPrejoin, makePrecallTest } from '../actions';
|
||||
|
||||
import Prejoin from './Prejoin';
|
||||
|
||||
@@ -33,7 +33,7 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
const { getState, dispatch } = store;
|
||||
const state = getState();
|
||||
const { userSelectedSkipPrejoin } = state['features/prejoin'];
|
||||
const localTracks = getLocalTracks(state['features/base/tracks']);
|
||||
let localTracks = getLocalTracks(state['features/base/tracks']);
|
||||
const { options } = action;
|
||||
|
||||
options && store.dispatch(updateConfig(options));
|
||||
@@ -48,6 +48,12 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
await dispatch(replaceLocalTrack(track.jitsiTrack, null));
|
||||
}
|
||||
}
|
||||
|
||||
// Re-fetch the local tracks after muted tracks have been removed above.
|
||||
// This is needed, because the tracks are effectively disposed by the replaceLocalTrack and should not be used
|
||||
// anymore.
|
||||
localTracks = getLocalTracks(getState()['features/base/tracks']);
|
||||
|
||||
const jitsiTracks = localTracks.map(t => t.jitsiTrack);
|
||||
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getDropboxData,
|
||||
isEnabled as isDropboxEnabled
|
||||
} from '../../../dropbox';
|
||||
import { showErrorNotification } from '../../../notifications';
|
||||
import { toggleRequestingSubtitles } from '../../../subtitles';
|
||||
import { setSelectedRecordingService } from '../../actions';
|
||||
import { RECORDING_TYPES } from '../../constants';
|
||||
@@ -254,19 +255,24 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
|
||||
let appData;
|
||||
const attributes = {};
|
||||
|
||||
if (_isDropboxEnabled
|
||||
&& _token
|
||||
&& this.state.selectedRecordingService
|
||||
=== RECORDING_TYPES.DROPBOX) {
|
||||
appData = JSON.stringify({
|
||||
'file_recording_metadata': {
|
||||
'upload_credentials': {
|
||||
'service_name': RECORDING_TYPES.DROPBOX,
|
||||
'token': _token
|
||||
if (_isDropboxEnabled && this.state.selectedRecordingService === RECORDING_TYPES.DROPBOX) {
|
||||
if (_token) {
|
||||
appData = JSON.stringify({
|
||||
'file_recording_metadata': {
|
||||
'upload_credentials': {
|
||||
'service_name': RECORDING_TYPES.DROPBOX,
|
||||
'token': _token
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
attributes.type = RECORDING_TYPES.DROPBOX;
|
||||
});
|
||||
attributes.type = RECORDING_TYPES.DROPBOX;
|
||||
} else {
|
||||
dispatch(showErrorNotification({
|
||||
titleKey: 'dialog.noDropboxToken'
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
appData = JSON.stringify({
|
||||
'file_recording_metadata': {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getYoutubeId } from '../functions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link AbstractSharedVideoDialog}.
|
||||
@@ -42,4 +44,27 @@ export default class AbstractSharedVideoDialog<S: *> extends Component < Props,
|
||||
}
|
||||
|
||||
_onSetVideoLink: string => boolean;
|
||||
|
||||
/**
|
||||
* Validates the entered video link by extracting the id and dispatches it.
|
||||
*
|
||||
* It returns a boolean to comply the Dialog behaviour:
|
||||
* {@code true} - the dialog should be closed.
|
||||
* {@code false} - the dialog should be left open.
|
||||
*
|
||||
* @param {string} link - The entered video link.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSetVideoLink(link: string) {
|
||||
if (!link || !link.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const youtubeId = getYoutubeId(link);
|
||||
const { onPostSubmit } = this.props;
|
||||
|
||||
onPostSubmit(youtubeId || link);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/* @flow */
|
||||
/* eslint-disable no-invalid-this */
|
||||
|
||||
import throttle from 'lodash/throttle';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { getCurrentConference } from '../../../base/conference';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { setSharedVideoStatus } from '../../actions.any';
|
||||
import { PLAYBACK_STATUSES } from '../../constants';
|
||||
|
||||
/**
|
||||
* Return true if the diffenrece between the two timees is larger than 5.
|
||||
*
|
||||
* @param {number} newTime - The current time.
|
||||
* @param {number} previousTime - The previous time.
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldSeekToPosition(newTime, previousTime) {
|
||||
return Math.abs(newTime - previousTime) > 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link AbstractVideoManager}.
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The current coference
|
||||
*/
|
||||
_conference: Object,
|
||||
|
||||
/**
|
||||
* Is the video shared by the local user.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_isOwner: boolean,
|
||||
|
||||
/**
|
||||
* The shared video owner id
|
||||
*/
|
||||
_ownerId: string,
|
||||
|
||||
/**
|
||||
* The shared video status
|
||||
*/
|
||||
_status: string,
|
||||
|
||||
/**
|
||||
* Seek time in seconds.
|
||||
*
|
||||
*/
|
||||
_time: number,
|
||||
|
||||
/**
|
||||
* The video url
|
||||
*/
|
||||
_videoUrl: string,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The player's height
|
||||
*/
|
||||
height: number,
|
||||
|
||||
/**
|
||||
* The video id
|
||||
*/
|
||||
videoId: string,
|
||||
|
||||
/**
|
||||
* The player's width
|
||||
*/
|
||||
width: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager of shared video.
|
||||
*/
|
||||
class AbstractVideoManager extends PureComponent<Props> {
|
||||
throttledFireUpdateSharedVideoEvent: Function;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of AbstractVideoManager.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.throttledFireUpdateSharedVideoEvent = throttle(this.fireUpdateSharedVideoEvent.bind(this), 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentDidMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.processUpdatedProps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentDidUpdate.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
this.processUpdatedProps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillUnmount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
if (this.dispose) {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes new properties.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async processUpdatedProps() {
|
||||
const { _status, _time, _isOwner } = this.props;
|
||||
|
||||
if (_isOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playerTime = await this.getTime();
|
||||
|
||||
if (shouldSeekToPosition(_time, playerTime)) {
|
||||
this.seek(_time);
|
||||
}
|
||||
|
||||
if (this.getPlaybackStatus() !== _status) {
|
||||
if (_status === PLAYBACK_STATUSES.PLAYING) {
|
||||
this.play();
|
||||
} else if (_status === PLAYBACK_STATUSES.PAUSED) {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle video playing.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
onPlay() {
|
||||
this.fireUpdateSharedVideoEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle video paused.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
onPause() {
|
||||
this.fireUpdateSharedVideoEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an update action for the shared video.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async fireUpdateSharedVideoEvent() {
|
||||
const { _isOwner } = this.props;
|
||||
|
||||
if (!_isOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = this.getPlaybackStatus();
|
||||
|
||||
if (!Object.values(PLAYBACK_STATUSES).includes(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const time = await this.getTime();
|
||||
|
||||
const {
|
||||
_ownerId,
|
||||
_videoUrl,
|
||||
dispatch
|
||||
} = this.props;
|
||||
|
||||
dispatch(setSharedVideoStatus({
|
||||
videoUrl: _videoUrl,
|
||||
status,
|
||||
time,
|
||||
ownerId: _ownerId
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks video to provided time
|
||||
* @param {number} time
|
||||
*/
|
||||
seek: (time: number) => void;
|
||||
|
||||
/**
|
||||
* Indicates the playback state of the video
|
||||
*/
|
||||
getPlaybackStatus: () => boolean;
|
||||
|
||||
/**
|
||||
* Plays video
|
||||
*/
|
||||
play: () => void;
|
||||
|
||||
/**
|
||||
* Pauses video
|
||||
*/
|
||||
pause: () => void;
|
||||
|
||||
/**
|
||||
* Retrieves current time
|
||||
*/
|
||||
getTime: () => number;
|
||||
|
||||
/**
|
||||
* Disposes current video player
|
||||
*/
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
|
||||
export default AbstractVideoManager;
|
||||
|
||||
/**
|
||||
* Maps part of the Redux store to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object): $Shape<Props> {
|
||||
const { ownerId, status, time, videoUrl } = state['features/shared-video'];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_conference: getCurrentConference(state),
|
||||
_isOwner: ownerId === localParticipant.id,
|
||||
_ownerId: ownerId,
|
||||
_status: status,
|
||||
_time: time,
|
||||
_videoUrl: videoUrl
|
||||
};
|
||||
}
|
||||
168
react/features/shared-video/components/native/SharedVideo.js
Normal file
168
react/features/shared-video/components/native/SharedVideo.js
Normal file
@@ -0,0 +1,168 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui';
|
||||
import { setToolboxVisible } from '../../../toolbox/actions';
|
||||
|
||||
import VideoManager from './VideoManager';
|
||||
import YoutubeVideoManager from './YoutubeVideoManager';
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Is the video shared by the local user.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
isOwner: boolean,
|
||||
|
||||
/**
|
||||
* True if in landscape mode.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
isWideScreen: boolean,
|
||||
|
||||
/**
|
||||
* The available player width
|
||||
*/
|
||||
playerHeight: number,
|
||||
|
||||
/**
|
||||
* The available player width
|
||||
*/
|
||||
playerWidth: number,
|
||||
|
||||
/**
|
||||
* The shared video url
|
||||
*/
|
||||
videoUrl: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which represents the large video (a.k.a.
|
||||
* the conference participant who is on the local stage) on Web/React.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class SharedVideo extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code SharedVideo} instance.
|
||||
*
|
||||
* @param {Object} props - The properties.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.setWideScreenMode(props.isWideScreen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidUpdate()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { isWideScreen } = this.props;
|
||||
|
||||
if (isWideScreen !== prevProps.isWideScreen) {
|
||||
this.setWideScreenMode(isWideScreen);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches action to set the visibility of the toolbox, true if not widescreen, false otherwise.
|
||||
*
|
||||
* @param {isWideScreen} isWideScreen - Whether the screen is wide.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
setWideScreenMode(isWideScreen) {
|
||||
this.props.dispatch(setToolboxVisible(!isWideScreen));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
isOwner,
|
||||
playerHeight,
|
||||
playerWidth,
|
||||
videoUrl
|
||||
} = this.props;
|
||||
|
||||
if (!videoUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
pointerEvents = { isOwner ? 'auto' : 'none' }
|
||||
style = { styles.videoContainer } >
|
||||
{videoUrl.match(/http/)
|
||||
? (
|
||||
<VideoManager
|
||||
height = { playerHeight }
|
||||
videoId = { videoUrl }
|
||||
width = { playerWidth } />
|
||||
) : (
|
||||
<YoutubeVideoManager
|
||||
height = { playerHeight }
|
||||
videoId = { videoUrl }
|
||||
width = { playerWidth } />
|
||||
)
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated LargeVideo props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { ownerId, videoUrl } = state['features/shared-video'];
|
||||
const { aspectRatio, clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
const isWideScreen = aspectRatio === ASPECT_RATIO_WIDE;
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
let playerHeight, playerWidth;
|
||||
|
||||
if (isWideScreen) {
|
||||
playerHeight = clientHeight;
|
||||
playerWidth = playerHeight * 16 / 9;
|
||||
} else {
|
||||
playerWidth = clientWidth;
|
||||
playerHeight = playerWidth * 9 / 16;
|
||||
}
|
||||
|
||||
return {
|
||||
isOwner: ownerId === localParticipant.id,
|
||||
isWideScreen,
|
||||
playerHeight,
|
||||
playerWidth,
|
||||
videoUrl
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(SharedVideo);
|
||||
@@ -4,14 +4,35 @@ import React from 'react';
|
||||
|
||||
import { InputDialog } from '../../../base/dialog';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { defaultMobileSharedVideoLink } from '../../constants';
|
||||
import { getYoutubeId } from '../../functions';
|
||||
import { defaultSharedVideoLink } from '../../constants';
|
||||
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
|
||||
|
||||
/**
|
||||
* Implements a component to render a display name prompt.
|
||||
*/
|
||||
class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onSubmitValue = this._onSubmitValue.bind(this);
|
||||
}
|
||||
|
||||
_onSubmitValue: () => boolean;
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the value of the link input is submitted.
|
||||
*
|
||||
* @param {string} value - The entered video link.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSubmitValue(value) {
|
||||
return super._onSetVideoLink(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
@@ -22,40 +43,12 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
|
||||
return (
|
||||
<InputDialog
|
||||
contentKey = 'dialog.shareVideoTitle'
|
||||
onSubmit = { this._onSetVideoLink }
|
||||
onSubmit = { this._onSubmitValue }
|
||||
textInputProps = {{
|
||||
placeholder: defaultMobileSharedVideoLink
|
||||
placeholder: defaultSharedVideoLink
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entered video link by extracting the id and dispatches it.
|
||||
*
|
||||
* It returns a boolean to comply the Dialog behaviour:
|
||||
* {@code true} - the dialog should be closed.
|
||||
* {@code false} - the dialog should be left open.
|
||||
*
|
||||
* @param {string} link - The entered video link.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSetVideoLink(link: string) {
|
||||
if (!link || !link.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const videoId = getYoutubeId(link);
|
||||
|
||||
if (videoId) {
|
||||
const { onPostSubmit } = this.props;
|
||||
|
||||
onPostSubmit && onPostSubmit(videoId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(SharedVideoDialog);
|
||||
|
||||
187
react/features/shared-video/components/native/VideoManager.js
Normal file
187
react/features/shared-video/components/native/VideoManager.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import React from 'react';
|
||||
import Video from 'react-native-video';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { PLAYBACK_STATUSES } from '../../constants';
|
||||
|
||||
import AbstractVideoManager, {
|
||||
_mapStateToProps,
|
||||
Props
|
||||
} from './AbstractVideoManager';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Manager of shared video.
|
||||
*/
|
||||
class VideoManager extends AbstractVideoManager<Props> {
|
||||
/**
|
||||
* Initializes a new VideoManager instance.
|
||||
*
|
||||
* @param {Object} props - This component's props.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
currentTime: 0,
|
||||
paused: false
|
||||
};
|
||||
|
||||
this.playerRef = React.createRef();
|
||||
this.onPlaybackRateChange = this.onPlaybackRateChange.bind(this);
|
||||
this.onProgress = this.onProgress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current player ref.
|
||||
*/
|
||||
get player() {
|
||||
return this.playerRef.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the playback state of the video.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getPlaybackStatus() {
|
||||
let status;
|
||||
|
||||
if (this.state.paused) {
|
||||
status = PLAYBACK_STATUSES.PAUSED;
|
||||
} else {
|
||||
status = PLAYBACK_STATUSES.PLAYING;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current time.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getTime() {
|
||||
return this.state.currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks video to provided time.
|
||||
*
|
||||
* @param {number} time - The time to seek to.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
seek(time) {
|
||||
if (this.player) {
|
||||
this.player.seek(time);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays video.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
play() {
|
||||
this.setState({
|
||||
paused: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses video.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
pause() {
|
||||
this.setState({
|
||||
paused: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles playback rate changed event.
|
||||
*
|
||||
* @param {Object} options.playbackRate - Playback rate: 1 - playing, 0 - paused, other - slowed down / sped up.
|
||||
* @returns {void}
|
||||
*/
|
||||
onPlaybackRateChange({ playbackRate }) {
|
||||
if (playbackRate === 0) {
|
||||
this.setState({
|
||||
paused: true
|
||||
}, () => {
|
||||
this.onPause();
|
||||
});
|
||||
}
|
||||
|
||||
if (playbackRate === 1) {
|
||||
this.setState({
|
||||
paused: false
|
||||
}, () => {
|
||||
this.onPlay();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles progress updarte event.
|
||||
*
|
||||
* @param {Object} options - Progress event options.
|
||||
* @returns {void}
|
||||
*/
|
||||
onProgress(options) {
|
||||
this.setState({ currentTime: options.currentTime });
|
||||
this.throttledFireUpdateSharedVideoEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves video tag params.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
getPlayerOptions() {
|
||||
const { _isOwner, videoId, width, height } = this.props;
|
||||
const { paused } = this.state;
|
||||
|
||||
const options = {
|
||||
paused,
|
||||
progressUpdateInterval: 5000,
|
||||
resizeMode: 'cover',
|
||||
style: {
|
||||
height,
|
||||
width
|
||||
},
|
||||
source: { uri: videoId },
|
||||
controls: _isOwner,
|
||||
pictureInPicture: false,
|
||||
onProgress: this.onProgress,
|
||||
onError: event => {
|
||||
logger.error('Error in the player:', event);
|
||||
}
|
||||
};
|
||||
|
||||
if (_isOwner) {
|
||||
options.onPlaybackRateChange = this.onPlaybackRateChange;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return (<Video
|
||||
ref = { this.playerRef }
|
||||
{ ...this.getPlayerOptions() } />);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(VideoManager);
|
||||
@@ -1,441 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component, createRef } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import YoutubePlayer from 'react-native-youtube-iframe';
|
||||
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui';
|
||||
import { setToolboxVisible } from '../../../toolbox/actions';
|
||||
import { setSharedVideoStatus } from '../../actions.native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Passed to the webviewProps in order to avoid the usage of the ios player on which we cannot hide the controls.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
const webviewUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; // eslint-disable-line max-len
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link YoutubeLargeVideo}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Display the youtube controls on the player.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_enableControls: boolean,
|
||||
|
||||
/**
|
||||
* Is the video shared by the local user.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_isOwner: boolean,
|
||||
|
||||
/**
|
||||
* The ID of the participant (to be) depicted by LargeVideo.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_isPlaying: string,
|
||||
|
||||
/**
|
||||
* Set to true when the status is set to stop and the view should not react to further changes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_isStopped: boolean,
|
||||
|
||||
/**
|
||||
* True if in landscape mode.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_isWideScreen: boolean,
|
||||
|
||||
/**
|
||||
* The id of the participant sharing the video.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_ownerId: string,
|
||||
|
||||
/**
|
||||
* The height of the player.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_playerHeight: number,
|
||||
|
||||
/**
|
||||
* The width of the player.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_playerWidth: number,
|
||||
|
||||
/**
|
||||
* Seek time in seconds.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_seek: number,
|
||||
|
||||
/**
|
||||
* The status of the player.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_status: string,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Youtube id of the video to be played.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
youtubeId: string
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Implements a React {@code Component} for showing a youtube video.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class YoutubeLargeVideo extends Component<Props, *> {
|
||||
/**
|
||||
* Saves a handle to the timer for seek time updates,
|
||||
* so that it can be cancelled when the component unmounts.
|
||||
*/
|
||||
intervalId: ?IntervalID;
|
||||
|
||||
/**
|
||||
* A React ref to the HTML element containing the {@code YoutubePlayer} instance.
|
||||
*/
|
||||
playerRef: Object;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code YoutubeLargeVideo} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.playerRef = createRef();
|
||||
|
||||
this._onReady = this._onReady.bind(this);
|
||||
this._onChangeState = this._onChangeState.bind(this);
|
||||
|
||||
this.setWideScreenMode(props._isWideScreen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to the new time if the difference between the new one and the current is larger than 5 seconds.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const playerRef = this.playerRef.current;
|
||||
const { _isWideScreen, _seek } = this.props;
|
||||
|
||||
if (_seek !== prevProps._seek) {
|
||||
playerRef && playerRef.getCurrentTime().then(time => {
|
||||
if (shouldSeekToPosition(_seek, time)) {
|
||||
playerRef && playerRef.seekTo(_seek);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (_isWideScreen !== prevProps._isWideScreen) {
|
||||
this.setWideScreenMode(_isWideScreen);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the interval for saving the seek time to redux every 5 seconds.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.saveRefTime();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the interval.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the YoutubeLargeVideo element.
|
||||
*
|
||||
* @override
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_enableControls,
|
||||
_isPlaying,
|
||||
_playerHeight,
|
||||
_playerWidth,
|
||||
youtubeId
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
pointerEvents = { _enableControls ? 'auto' : 'none' }
|
||||
style = { styles.youtubeVideoContainer } >
|
||||
<YoutubePlayer
|
||||
height = { _playerHeight }
|
||||
initialPlayerParams = {{
|
||||
controls: _enableControls,
|
||||
modestbranding: true,
|
||||
preventFullScreen: true
|
||||
}}
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onChangeState = { this._onChangeState }
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onReady = { this._onReady }
|
||||
play = { _isPlaying }
|
||||
playbackRate = { 1 }
|
||||
ref = { this.playerRef }
|
||||
videoId = { youtubeId }
|
||||
volume = { 50 }
|
||||
webViewProps = {{
|
||||
bounces: false,
|
||||
mediaPlaybackRequiresUserAction: false,
|
||||
scrollEnabled: false,
|
||||
userAgent: webviewUserAgent
|
||||
}}
|
||||
width = { _playerWidth } />
|
||||
</View>);
|
||||
}
|
||||
|
||||
_onReady: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked when the player is ready to play the video.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onReady() {
|
||||
if (this.props?._isOwner) {
|
||||
this.onVideoReady(
|
||||
this.props.youtubeId,
|
||||
this.playerRef.current && this.playerRef.current.getCurrentTime(),
|
||||
this.props._ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
_onChangeState: (status: string) => void;
|
||||
|
||||
/**
|
||||
* Callback invoked when the state of the player changes.
|
||||
*
|
||||
* @param {string} status - The new status of the player.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeState(status) {
|
||||
this.playerRef?.current && this.playerRef.current.getCurrentTime().then(time => {
|
||||
const {
|
||||
_isOwner,
|
||||
_isPlaying,
|
||||
_isStopped,
|
||||
_ownerId,
|
||||
_seek,
|
||||
youtubeId
|
||||
} = this.props;
|
||||
|
||||
if (shouldSetNewStatus(_isStopped, _isOwner, status, _isPlaying, time, _seek)) {
|
||||
this.onVideoChangeEvent(youtubeId, status, time, _ownerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls onVideoChangeEvent with the refTime.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
saveRefTime() {
|
||||
const { youtubeId, _status, _ownerId } = this.props;
|
||||
|
||||
this.playerRef.current && this.playerRef.current.getCurrentTime().then(time => {
|
||||
this.onVideoChangeEvent(youtubeId, _status, time, _ownerId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the video status, time and ownerId if the status is playing or paused.
|
||||
*
|
||||
* @param {string} videoUrl - The youtube id of the video.
|
||||
* @param {string} status - The status of the player.
|
||||
* @param {number} time - The seek time.
|
||||
* @param {string} ownerId - The id of the participant sharing the video.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
onVideoChangeEvent(videoUrl, status, time, ownerId) {
|
||||
if (![ 'playing', 'paused' ].includes(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch(setSharedVideoStatus({
|
||||
videoUrl,
|
||||
status: translateStatus(status),
|
||||
time,
|
||||
ownerId
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the 'playing' as video status, time and ownerId.
|
||||
*
|
||||
* @param {string} videoUrl - The youtube id of the video.
|
||||
* @param {number} time - The seek time.
|
||||
* @param {string} ownerId - The id of the participant sharing the video.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
onVideoReady(videoUrl, time, ownerId) {
|
||||
time.then(t => this.props.dispatch(setSharedVideoStatus({
|
||||
videoUrl,
|
||||
status: 'playing',
|
||||
time: t,
|
||||
ownerId
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches action to set the visibility of the toolbox, true if not widescreen, false otherwise.
|
||||
*
|
||||
* @param {isWideScreen} isWideScreen - Whether the screen is wide.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
setWideScreenMode(isWideScreen) {
|
||||
this.props.dispatch(setToolboxVisible(!isWideScreen));
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable max-params */
|
||||
|
||||
/**
|
||||
* Return true if the user is the owner and
|
||||
* the status has changed or the seek time difference from the previous set is larger than 5 seconds.
|
||||
*
|
||||
* @param {boolean} isStopped - Once the status was set to stop, all the other statuses should be ignored.
|
||||
* @param {boolean} isOwner - Whether the local user is sharing the video.
|
||||
* @param {string} status - The new status.
|
||||
* @param {boolean} isPlaying - Whether the component is playing at the moment.
|
||||
* @param {number} newTime - The new seek time.
|
||||
* @param {number} previousTime - The old seek time.
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldSetNewStatus(isStopped, isOwner, status, isPlaying, newTime, previousTime) {
|
||||
if (isStopped) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isOwner || status === 'buffering') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((isPlaying && status === 'paused') || (!isPlaying && status === 'playing')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return shouldSeekToPosition(newTime, previousTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the diffenrece between the two timees is larger than 5.
|
||||
*
|
||||
* @param {number} newTime - The current time.
|
||||
* @param {number} previousTime - The previous time.
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldSeekToPosition(newTime, previousTime) {
|
||||
return Math.abs(newTime - previousTime) > 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated YoutubeLargeVideo's props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { ownerId, status, time } = state['features/shared-video'];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const responsiveUi = state['features/base/responsive-ui'];
|
||||
const { aspectRatio, clientHeight: screenHeight, clientWidth: screenWidth } = responsiveUi;
|
||||
const isWideScreen = aspectRatio === ASPECT_RATIO_WIDE;
|
||||
|
||||
let playerHeight, playerWidth;
|
||||
|
||||
if (isWideScreen) {
|
||||
playerHeight = screenHeight;
|
||||
playerWidth = playerHeight * 16 / 9;
|
||||
} else {
|
||||
playerWidth = screenWidth;
|
||||
playerHeight = playerWidth * 9 / 16;
|
||||
}
|
||||
|
||||
return {
|
||||
_enableControls: ownerId === localParticipant.id,
|
||||
_isOwner: ownerId === localParticipant.id,
|
||||
_isPlaying: status === 'playing',
|
||||
_isStopped: status === 'stop',
|
||||
_isWideScreen: isWideScreen,
|
||||
_ownerId: ownerId,
|
||||
_playerHeight: playerHeight,
|
||||
_playerWidth: playerWidth,
|
||||
_seek: time,
|
||||
_status: status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* In case the status is 'paused', it is translated to 'pause' to match the web functionality.
|
||||
*
|
||||
* @param {string} status - The status of the shared video.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
function translateStatus(status) {
|
||||
if (status === 'paused') {
|
||||
return 'pause';
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(YoutubeLargeVideo);
|
||||
@@ -0,0 +1,193 @@
|
||||
import React from 'react';
|
||||
import Video from 'react-native-youtube-iframe';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { PLAYBACK_STATUSES } from '../../constants';
|
||||
|
||||
import AbstractVideoManager, {
|
||||
_mapStateToProps,
|
||||
Props
|
||||
} from './AbstractVideoManager';
|
||||
|
||||
/**
|
||||
* Passed to the webviewProps in order to avoid the usage of the ios player on which we cannot hide the controls.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
const webviewUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; // eslint-disable-line max-len
|
||||
|
||||
/**
|
||||
* Manager of youtube shared video.
|
||||
*/
|
||||
class YoutubeVideoManager extends AbstractVideoManager<Props> {
|
||||
/**
|
||||
* Initializes a new VideoManager instance.
|
||||
*
|
||||
* @param {Object} props - This component's props.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
paused: false
|
||||
};
|
||||
|
||||
this.playerRef = React.createRef();
|
||||
this._onReady = this._onReady.bind(this);
|
||||
this._onChangeState = this._onChangeState.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current player ref.
|
||||
*/
|
||||
get player() {
|
||||
return this.playerRef.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the playback state of the video.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getPlaybackStatus() {
|
||||
let status;
|
||||
|
||||
if (this.state.paused) {
|
||||
status = PLAYBACK_STATUSES.PAUSED;
|
||||
} else {
|
||||
status = PLAYBACK_STATUSES.PLAYING;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current time.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getTime() {
|
||||
return this.player?.getCurrentTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks video to provided time.
|
||||
*
|
||||
* @param {number} time - The time to seek to.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
seek(time) {
|
||||
if (this.player) {
|
||||
this.player.seekTo(time);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays video.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
play() {
|
||||
this.setState({
|
||||
paused: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses video.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
pause() {
|
||||
this.setState({
|
||||
paused: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles state change event.
|
||||
*
|
||||
* @param {string} event - State event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeState(event) {
|
||||
if (event === 'paused') {
|
||||
this.setState({
|
||||
paused: true
|
||||
}, () => {
|
||||
this.onPause();
|
||||
});
|
||||
}
|
||||
|
||||
if (event === 'playing') {
|
||||
this.setState({
|
||||
paused: false
|
||||
}, () => {
|
||||
this.onPlay();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles onReady event.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onReady() {
|
||||
this.setState({
|
||||
paused: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves video tag params.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
getPlayerOptions() {
|
||||
const { _isOwner, videoId, width, height } = this.props;
|
||||
|
||||
const options = {
|
||||
height,
|
||||
initialPlayerParams: {
|
||||
controls: _isOwner,
|
||||
modestbranding: true,
|
||||
preventFullScreen: true
|
||||
},
|
||||
play: !this.state.paused,
|
||||
ref: this.playerRef,
|
||||
videoId,
|
||||
volume: 50,
|
||||
webViewProps: {
|
||||
bounces: false,
|
||||
mediaPlaybackRequiresUserAction: false,
|
||||
scrollEnabled: false,
|
||||
userAgent: webviewUserAgent
|
||||
},
|
||||
width
|
||||
};
|
||||
|
||||
if (_isOwner) {
|
||||
options.onChangeState = this._onChangeState;
|
||||
options.onReady = this._onReady;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return (<Video
|
||||
ref = { this.playerRef }
|
||||
{ ...this.getPlayerOptions() } />);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(YoutubeVideoManager);
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
export { default as SharedVideoButton } from './SharedVideoButton';
|
||||
export { default as SharedVideoDialog } from './SharedVideoDialog';
|
||||
export { default as YoutubeLargeVideo } from './YoutubeLargeVideo';
|
||||
export { default as SharedVideo } from './SharedVideo';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* The style of toolbar buttons.
|
||||
*/
|
||||
export default {
|
||||
youtubeVideoContainer: {
|
||||
videoContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* eslint-disable no-invalid-this */
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { Component } from 'react';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { sendAnalytics, createSharedVideoEvent as createEvent } from '../../../analytics';
|
||||
import { getCurrentConference } from '../../../base/conference';
|
||||
@@ -13,11 +13,7 @@ import { showWarningNotification } from '../../../notifications/actions';
|
||||
import { dockToolbox } from '../../../toolbox/actions.web';
|
||||
import { muteLocal } from '../../../video-menu/actions.any';
|
||||
import { setSharedVideoStatus, stopSharedVideo } from '../../actions.any';
|
||||
export const PLAYBACK_STATES = {
|
||||
PLAYING: 'playing',
|
||||
PAUSED: 'pause',
|
||||
STOPPED: 'stop'
|
||||
};
|
||||
import { PLAYBACK_STATUSES } from '../../constants';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
@@ -34,7 +30,7 @@ function shouldSeekToPosition(newTime, previousTime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link YoutubeLargeVideo}.
|
||||
* The type of the React {@link PureComponent} props of {@link AbstractVideoManager}.
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
@@ -115,7 +111,7 @@ export type Props = {
|
||||
/**
|
||||
* Manager of shared video.
|
||||
*/
|
||||
class AbstractVideoManager extends Component<Props> {
|
||||
class AbstractVideoManager extends PureComponent<Props> {
|
||||
throttledFireUpdateSharedVideoEvent: Function;
|
||||
|
||||
/**
|
||||
@@ -190,12 +186,12 @@ class AbstractVideoManager extends Component<Props> {
|
||||
this.seek(_time);
|
||||
}
|
||||
|
||||
if (this.getPlaybackState() !== _status) {
|
||||
if (_status === PLAYBACK_STATES.PLAYING) {
|
||||
if (this.getPlaybackStatus() !== _status) {
|
||||
if (_status === PLAYBACK_STATUSES.PLAYING) {
|
||||
this.play();
|
||||
}
|
||||
|
||||
if (_status === PLAYBACK_STATES.PAUSED) {
|
||||
if (_status === PLAYBACK_STATUSES.PAUSED) {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
@@ -270,7 +266,7 @@ class AbstractVideoManager extends Component<Props> {
|
||||
* @returns {void}
|
||||
*/
|
||||
fireUpdatePlayingVideoEvent() {
|
||||
if (this.getPlaybackState() === PLAYBACK_STATES.PLAYING) {
|
||||
if (this.getPlaybackStatus() === PLAYBACK_STATUSES.PLAYING) {
|
||||
this.fireUpdateSharedVideoEvent();
|
||||
}
|
||||
}
|
||||
@@ -287,9 +283,9 @@ class AbstractVideoManager extends Component<Props> {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = this.getPlaybackState();
|
||||
const status = this.getPlaybackStatus();
|
||||
|
||||
if (!Object.values(PLAYBACK_STATES).includes(status)) {
|
||||
if (!Object.values(PLAYBACK_STATUSES).includes(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -317,7 +313,7 @@ class AbstractVideoManager extends Component<Props> {
|
||||
* currently on.
|
||||
*/
|
||||
isSharedVideoVolumeOn() {
|
||||
return this.getPlaybackState() === PLAYBACK_STATES.PLAYING
|
||||
return this.getPlaybackStatus() === PLAYBACK_STATUSES.PLAYING
|
||||
&& !this.isMuted()
|
||||
&& this.getVolume() > 0;
|
||||
}
|
||||
@@ -347,7 +343,7 @@ class AbstractVideoManager extends Component<Props> {
|
||||
/**
|
||||
* Indicates the playback state of the video
|
||||
*/
|
||||
getPlaybackState: () => boolean;
|
||||
getPlaybackStatus: () => boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether the video is muted
|
||||
@@ -359,11 +355,6 @@ class AbstractVideoManager extends Component<Props> {
|
||||
*/
|
||||
getVolume: () => number;
|
||||
|
||||
/**
|
||||
* Sets current volume
|
||||
*/
|
||||
setVolume: (value: number) => void;
|
||||
|
||||
/**
|
||||
* Plays video
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,6 @@ import { translate } from '../../../base/i18n';
|
||||
import { getFieldValue } from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { defaultSharedVideoLink } from '../../constants';
|
||||
import { getYoutubeId } from '../../functions';
|
||||
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
|
||||
|
||||
/**
|
||||
@@ -60,7 +59,7 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSubmitValue() {
|
||||
return this._onSetVideoLink(this.state.value);
|
||||
return super._onSetVideoLink(this.state.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,29 +93,6 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entered video link by extracting the id and dispatches it.
|
||||
*
|
||||
* It returns a boolean to comply the Dialog behaviour:
|
||||
* {@code true} - the dialog should be closed.
|
||||
* {@code false} - the dialog should be left open.
|
||||
*
|
||||
* @param {string} link - The entered video link.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSetVideoLink(link: string) {
|
||||
if (!link || !link.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const youtubeId = getYoutubeId(link);
|
||||
const { onPostSubmit } = this.props;
|
||||
|
||||
onPostSubmit(youtubeId || link);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_onChange: Object => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { PLAYBACK_STATUSES } from '../../constants';
|
||||
|
||||
import AbstractVideoManager, {
|
||||
_mapDispatchToProps,
|
||||
_mapStateToProps,
|
||||
PLAYBACK_STATES,
|
||||
Props
|
||||
} from './AbstractVideoManager';
|
||||
|
||||
@@ -39,20 +39,20 @@ class VideoManager extends AbstractVideoManager<Props> {
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getPlaybackState() {
|
||||
let state;
|
||||
getPlaybackStatus() {
|
||||
let status;
|
||||
|
||||
if (!this.player) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player.paused) {
|
||||
state = PLAYBACK_STATES.PAUSED;
|
||||
status = PLAYBACK_STATUSES.PAUSED;
|
||||
} else {
|
||||
state = PLAYBACK_STATES.PLAYING;
|
||||
status = PLAYBACK_STATUSES.PLAYING;
|
||||
}
|
||||
|
||||
return state;
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,19 +73,6 @@ class VideoManager extends AbstractVideoManager<Props> {
|
||||
return this.player?.volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets player volume.
|
||||
*
|
||||
* @param {number} value - The volume.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
setVolume(value) {
|
||||
if (this.player) {
|
||||
this.player.volume = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current time.
|
||||
*
|
||||
|
||||
@@ -3,11 +3,11 @@ import React from 'react';
|
||||
import YouTube from 'react-youtube';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { PLAYBACK_STATUSES } from '../../constants';
|
||||
|
||||
import AbstractVideoManager, {
|
||||
_mapDispatchToProps,
|
||||
_mapStateToProps,
|
||||
PLAYBACK_STATES
|
||||
_mapStateToProps
|
||||
} from './AbstractVideoManager';
|
||||
|
||||
/**
|
||||
@@ -34,8 +34,8 @@ class YoutubeVideoManager extends AbstractVideoManager<Props> {
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getPlaybackState() {
|
||||
let state;
|
||||
getPlaybackStatus() {
|
||||
let status;
|
||||
|
||||
if (!this.player) {
|
||||
return;
|
||||
@@ -44,14 +44,14 @@ class YoutubeVideoManager extends AbstractVideoManager<Props> {
|
||||
const playerState = this.player.getPlayerState();
|
||||
|
||||
if (playerState === YouTube.PlayerState.PLAYING) {
|
||||
state = PLAYBACK_STATES.PLAYING;
|
||||
status = PLAYBACK_STATUSES.PLAYING;
|
||||
}
|
||||
|
||||
if (playerState === YouTube.PlayerState.PAUSED) {
|
||||
state = PLAYBACK_STATES.PAUSED;
|
||||
status = PLAYBACK_STATUSES.PAUSED;
|
||||
}
|
||||
|
||||
return state;
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,17 +72,6 @@ class YoutubeVideoManager extends AbstractVideoManager<Props> {
|
||||
return this.player?.getVolume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets player volume.
|
||||
*
|
||||
* @param {number} value - The volume.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
setVolume(value) {
|
||||
return this.player?.setVolume(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current time.
|
||||
*
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
*/
|
||||
export const defaultSharedVideoLink = 'Youtube link or direct video link';
|
||||
|
||||
/**
|
||||
* Mobile example for a youtube video
|
||||
*/
|
||||
export const defaultMobileSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
|
||||
|
||||
/**
|
||||
* Fixed name of the video player fake participant.
|
||||
* @type {string}
|
||||
@@ -29,3 +24,12 @@ export const YOUTUBE_PLAYER_PARTICIPANT_NAME = 'YouTube';
|
||||
* @type {string}
|
||||
*/
|
||||
export const SHARED_VIDEO = 'shared-video';
|
||||
|
||||
/**
|
||||
* Available playback statuses
|
||||
*/
|
||||
export const PLAYBACK_STATUSES = {
|
||||
PLAYING: 'playing',
|
||||
PAUSED: 'pause',
|
||||
STOPPED: 'stop'
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Divider } from 'react-native-paper';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
|
||||
@@ -8,7 +9,6 @@ import { getFeatureFlag, REACTIONS_ENABLED } from '../../../base/flags';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
import { SharedDocumentButton } from '../../../etherpad';
|
||||
import { InviteButton } from '../../../invite';
|
||||
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
||||
import { ParticipantsPaneButton } from '../../../participants-pane/components/native';
|
||||
import { ReactionMenu } from '../../../reactions/components';
|
||||
@@ -17,6 +17,7 @@ import SecurityDialogButton from '../../../security/components/security-dialog/S
|
||||
import { SharedVideoButton } from '../../../shared-video/components';
|
||||
import { ClosedCaptionButton } from '../../../subtitles';
|
||||
import { TileViewButton } from '../../../video-layout';
|
||||
import styles from '../../../video-menu/components/native/styles';
|
||||
import { getMovableButtons } from '../../functions.native';
|
||||
import HelpButton from '../HelpButton';
|
||||
import MuteEveryoneButton from '../MuteEveryoneButton';
|
||||
@@ -126,8 +127,7 @@ class OverflowMenu extends PureComponent<Props, State> {
|
||||
style: {
|
||||
..._bottomSheetStyles.buttons.style,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
paddingTop: 16
|
||||
borderTopRightRadius: 16
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -140,20 +140,22 @@ class OverflowMenu extends PureComponent<Props, State> {
|
||||
: null }>
|
||||
<AudioRouteButton { ...topButtonProps } />
|
||||
<ParticipantsPaneButton { ...buttonProps } />
|
||||
{!toolbarButtons.has('invite') && <InviteButton { ...buttonProps } />}
|
||||
<AudioOnlyButton { ...buttonProps } />
|
||||
{!_reactionsEnabled && !toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />}
|
||||
<Divider style = { styles.divider } />
|
||||
<SecurityDialogButton { ...buttonProps } />
|
||||
<RecordButton { ...buttonProps } />
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<MuteEveryoneButton { ...buttonProps } />
|
||||
<MuteEveryonesVideoButton { ...buttonProps } />
|
||||
<Divider style = { styles.divider } />
|
||||
<SharedVideoButton { ...buttonProps } />
|
||||
<ScreenSharingButton { ...buttonProps } />
|
||||
{!toolbarButtons.has('togglecamera') && <ToggleCameraButton { ...buttonProps } />}
|
||||
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
|
||||
<RecordButton { ...buttonProps } />
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<SharedVideoButton { ...buttonProps } />
|
||||
<Divider style = { styles.divider } />
|
||||
<ClosedCaptionButton { ...buttonProps } />
|
||||
<SharedDocumentButton { ...buttonProps } />
|
||||
<MuteEveryoneButton { ...buttonProps } />
|
||||
<MuteEveryonesVideoButton { ...buttonProps } />
|
||||
<HelpButton { ...buttonProps } />
|
||||
</BottomSheet>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getFeatureFlag, REACTIONS_ENABLED } from '../../../base/flags';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
import { ChatButton } from '../../../chat';
|
||||
import { InviteButton } from '../../../invite';
|
||||
import { ParticipantsPaneButton } from '../../../participants-pane/components/native';
|
||||
import { ReactionsMenuButton } from '../../../reactions/components';
|
||||
import { TileViewButton } from '../../../video-layout';
|
||||
import { isToolboxVisible, getMovableButtons } from '../../functions.native';
|
||||
@@ -101,7 +101,10 @@ function Toolbox(props: Props) {
|
||||
styles = { buttonStylesBorderless }
|
||||
toggledStyles = { backgroundToggledStyle } />)}
|
||||
{additionalButtons.has('tileview') && <TileViewButton styles = { buttonStylesBorderless } />}
|
||||
{additionalButtons.has('invite') && <InviteButton styles = { buttonStylesBorderless } />}
|
||||
{additionalButtons.has('participantspane')
|
||||
&& <ParticipantsPaneButton
|
||||
styles = { buttonStylesBorderless } />
|
||||
}
|
||||
{additionalButtons.has('togglecamera')
|
||||
&& <ToggleCameraButton
|
||||
styles = { buttonStylesBorderless }
|
||||
|
||||
@@ -122,7 +122,7 @@ const mapStateToProps = state => {
|
||||
if (enableFeaturesBasedOnToken) {
|
||||
// we enable desktop sharing if any participant already have this
|
||||
// feature enabled
|
||||
desktopSharingEnabled = state['features/base/participant'].haveParticipantWithScreenSharingFeature;
|
||||
desktopSharingEnabled = state['features/base/participants'].haveParticipantWithScreenSharingFeature;
|
||||
desktopSharingDisabledTooltipKey = 'dialog.shareYourScreenDisabled';
|
||||
}
|
||||
|
||||
|
||||
75
react/features/toolbox/components/web/ToggleCameraButton.js
Normal file
75
react/features/toolbox/components/web/ToggleCameraButton.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// @flow
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconCameraRefresh } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
import { isLocalCameraTrackMuted, isToggleCameraEnabled, toggleCamera } from '../../../base/tracks';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ToggleCameraButton}.
|
||||
*/
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* Whether the current conference is in audio only mode or not.
|
||||
*/
|
||||
_audioOnly: boolean,
|
||||
|
||||
/**
|
||||
* Whether video is currently muted or not.
|
||||
*/
|
||||
_videoMuted: boolean,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* An implementation of a button for toggling the camera facing mode.
|
||||
*/
|
||||
class ToggleCameraButton extends AbstractButton<Props, any> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera';
|
||||
icon = IconCameraRefresh;
|
||||
label = 'toolbar.toggleCamera';
|
||||
|
||||
/**
|
||||
* Handles clicking/pressing the button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
this.props.dispatch(toggleCamera());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this button is disabled or not.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isDisabled() {
|
||||
return this.props._audioOnly || this.props._videoMuted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for the
|
||||
* {@code ToggleCameraButton} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function mapStateToProps(state): Object {
|
||||
const { enabled: audioOnly } = state['features/base/audio-only'];
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
return {
|
||||
_audioOnly: Boolean(audioOnly),
|
||||
_videoMuted: isLocalCameraTrackMuted(tracks),
|
||||
visible: isToggleCameraEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(ToggleCameraButton));
|
||||
@@ -73,7 +73,7 @@ import {
|
||||
setToolbarHovered,
|
||||
showToolbox
|
||||
} from '../../actions';
|
||||
import { THRESHOLDS } from '../../constants';
|
||||
import { THRESHOLDS, NOT_APPLICABLE } from '../../constants';
|
||||
import { isToolboxVisible } from '../../functions';
|
||||
import DownloadButton from '../DownloadButton';
|
||||
import HangupButton from '../HangupButton';
|
||||
@@ -88,6 +88,7 @@ import ProfileButton from './ProfileButton';
|
||||
import RaiseHandButton from './RaiseHandButton';
|
||||
import Separator from './Separator';
|
||||
import ShareDesktopButton from './ShareDesktopButton';
|
||||
import ToggleCameraButton from './ToggleCameraButton';
|
||||
import VideoSettingsButton from './VideoSettingsButton';
|
||||
|
||||
/**
|
||||
@@ -597,6 +598,7 @@ class Toolbox extends Component<Props> {
|
||||
|
||||
const participants = {
|
||||
key: 'participants-pane',
|
||||
alias: 'invite',
|
||||
Content: ParticipantsPaneButton,
|
||||
handleClick: this._onToolbarToggleParticipantsPane,
|
||||
group: 2
|
||||
@@ -608,6 +610,12 @@ class Toolbox extends Component<Props> {
|
||||
group: 2
|
||||
};
|
||||
|
||||
const toggleCamera = {
|
||||
key: 'toggle-camera',
|
||||
Content: ToggleCameraButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
const videoQuality = {
|
||||
key: 'videoquality',
|
||||
Content: VideoQualityButton,
|
||||
@@ -624,12 +632,7 @@ class Toolbox extends Component<Props> {
|
||||
|
||||
const security = {
|
||||
key: 'security',
|
||||
Content: SecurityDialogButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
const info = {
|
||||
key: 'info',
|
||||
alias: 'info',
|
||||
Content: SecurityDialogButton,
|
||||
group: 2
|
||||
};
|
||||
@@ -652,6 +655,12 @@ class Toolbox extends Component<Props> {
|
||||
group: 2
|
||||
};
|
||||
|
||||
const livestreaming = {
|
||||
key: 'livestreaming',
|
||||
Content: LiveStreamButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
const muteEveryone = {
|
||||
key: 'mute-everyone',
|
||||
Content: MuteEveryoneButton,
|
||||
@@ -664,12 +673,6 @@ class Toolbox extends Component<Props> {
|
||||
group: 2
|
||||
};
|
||||
|
||||
const livestreaming = {
|
||||
key: 'livestreaming',
|
||||
Content: LiveStreamButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
const shareVideo = {
|
||||
key: 'sharedvideo',
|
||||
Content: SharedVideoButton,
|
||||
@@ -745,16 +748,16 @@ class Toolbox extends Component<Props> {
|
||||
raisehand,
|
||||
participants,
|
||||
tileview,
|
||||
toggleCamera,
|
||||
videoQuality,
|
||||
fullscreen,
|
||||
security,
|
||||
info,
|
||||
cc,
|
||||
recording,
|
||||
localRecording,
|
||||
livestreaming,
|
||||
muteEveryone,
|
||||
muteVideoEveryone,
|
||||
livestreaming,
|
||||
shareVideo,
|
||||
shareAudio,
|
||||
etherpad,
|
||||
@@ -793,7 +796,8 @@ class Toolbox extends Component<Props> {
|
||||
const filtered = [
|
||||
...order.map(key => buttons[key]),
|
||||
...Object.values(buttons).filter((button, index) => !order.includes(keys[index]))
|
||||
].filter(Boolean).filter(({ key }) => isToolbarButtonEnabled(key, _toolbarButtons));
|
||||
].filter(Boolean).filter(({ key, alias = NOT_APPLICABLE }) =>
|
||||
isToolbarButtonEnabled(key, _toolbarButtons) || isToolbarButtonEnabled(alias, _toolbarButtons));
|
||||
|
||||
if (isHangupVisible) {
|
||||
sliceIndex -= 1;
|
||||
@@ -1175,8 +1179,10 @@ class Toolbox extends Component<Props> {
|
||||
<div
|
||||
className = 'toolbox-content-wrapper'
|
||||
onFocus = { this._onTabIn }
|
||||
onMouseOut = { this._onMouseOut }
|
||||
onMouseOver = { this._onMouseOver }>
|
||||
{ ...(_isMobile ? {} : {
|
||||
onMouseOut: this._onMouseOut,
|
||||
onMouseOver: this._onMouseOver
|
||||
}) }>
|
||||
<DominantSpeakerName />
|
||||
<div className = 'toolbox-content-items'>
|
||||
{mainMenuButtons.map(({ Content, key, ...rest }) => Content !== Separator && (
|
||||
|
||||
@@ -27,3 +27,5 @@ export const THRESHOLDS = [
|
||||
order: [ 'microphone', 'camera' ]
|
||||
}
|
||||
];
|
||||
|
||||
export const NOT_APPLICABLE = 'N/A';
|
||||
|
||||
@@ -25,7 +25,7 @@ export function getMovableButtons(width: number): Set<string> {
|
||||
|
||||
switch (true) {
|
||||
case width >= WIDTH.FIT_9_ICONS: {
|
||||
buttons = [ 'togglecamera', 'chat', 'invite', 'raisehand', 'tileview' ];
|
||||
buttons = [ 'togglecamera', 'chat', 'participantspane', 'raisehand', 'tileview' ];
|
||||
break;
|
||||
}
|
||||
case width >= WIDTH.FIT_8_ICONS: {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
getRemoteParticipants,
|
||||
muteRemoteParticipant
|
||||
} from '../base/participants';
|
||||
import { getIsParticipantAudioMuted } from '../base/tracks';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -108,18 +107,3 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Don't allow participants to unmute video/audio.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function blockParticipantsAudioVideo() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const participants = state['features/base/participants'];
|
||||
|
||||
participants
|
||||
.map(p => !getIsParticipantAudioMuted(p) && setAudioMuted(true));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import { hideDialog } from '../base/dialog';
|
||||
|
||||
import { RemoteVideoMenu } from './components/native';
|
||||
import { RemoteVideoMenu, SharedVideoMenu } from './components/native';
|
||||
|
||||
/**
|
||||
* Hides the remote video menu.
|
||||
@@ -12,4 +12,13 @@ export function hideRemoteVideoMenu() {
|
||||
return hideDialog(RemoteVideoMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the shared video menu.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hideSharedVideoMenu() {
|
||||
return hideDialog(SharedVideoMenu);
|
||||
}
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { IconKick } from '../../base/icons';
|
||||
import { IconCloseCircle } from '../../base/icons';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||
|
||||
import { KickRemoteParticipantDialog } from '.';
|
||||
@@ -29,7 +29,7 @@ export type Props = AbstractButtonProps & {
|
||||
*/
|
||||
export default class AbstractKickButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.kick';
|
||||
icon = IconKick;
|
||||
icon = IconCloseCircle;
|
||||
label = 'videothumbnail.kick';
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { IconCameraDisabled } from '../../base/icons';
|
||||
import { IconVideoOff } from '../../base/icons';
|
||||
import { MEDIA_TYPE } from '../../base/media';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||
import { isRemoteTrackMuted } from '../../base/tracks';
|
||||
@@ -42,7 +42,7 @@ export type Props = AbstractButtonProps & {
|
||||
*/
|
||||
export default class AbstractMuteVideoButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.remoteVideoMute';
|
||||
icon = IconCameraDisabled;
|
||||
icon = IconVideoOff;
|
||||
label = 'videothumbnail.domuteVideo';
|
||||
toggledLabel = 'videothumbnail.videoMuted';
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { withTheme } from 'react-native-paper';
|
||||
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
@@ -11,7 +13,7 @@ import { IconArrowDownLarge, IconArrowUpLarge } from '../../../base/icons';
|
||||
import { getParticipantDisplayName } from '../../../base/participants';
|
||||
import { BaseIndicator } from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType, ColorPalette } from '../../../base/styles';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
import statsEmitter from '../../../connection-indicator/statsEmitter';
|
||||
|
||||
import styles from './styles';
|
||||
@@ -57,7 +59,12 @@ export type Props = {
|
||||
/**
|
||||
* The function to be used to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Theme used for styles.
|
||||
*/
|
||||
theme: Object
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +123,8 @@ class ConnectionStatusComponent extends Component<Props, State> {
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
render(): React$Node {
|
||||
const { t } = this.props;
|
||||
const { t, theme } = this.props;
|
||||
const { palette } = theme;
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
@@ -138,7 +146,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
|
||||
<BaseIndicator
|
||||
icon = { IconArrowDownLarge }
|
||||
iconStyle = {{
|
||||
color: ColorPalette.darkGrey
|
||||
color: palette.icon03
|
||||
}} />
|
||||
<Text style = { styles.statsInfoText }>
|
||||
{ this.state.downloadString }
|
||||
@@ -146,7 +154,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
|
||||
<BaseIndicator
|
||||
icon = { IconArrowUpLarge }
|
||||
iconStyle = {{
|
||||
color: ColorPalette.darkGrey
|
||||
color: palette.icon03
|
||||
}} />
|
||||
<Text style = { styles.statsInfoText }>
|
||||
{ `${this.state.uploadString} Kbps` }
|
||||
@@ -159,7 +167,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
|
||||
<BaseIndicator
|
||||
icon = { IconArrowDownLarge }
|
||||
iconStyle = {{
|
||||
color: ColorPalette.darkGrey
|
||||
color: palette.icon03
|
||||
}} />
|
||||
<Text style = { styles.statsInfoText }>
|
||||
{ this.state.packetLostDownloadString }
|
||||
@@ -167,7 +175,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
|
||||
<BaseIndicator
|
||||
icon = { IconArrowUpLarge }
|
||||
iconStyle = {{
|
||||
color: ColorPalette.darkGrey
|
||||
color: palette.icon03
|
||||
}} />
|
||||
<Text style = { styles.statsInfoText }>
|
||||
{ this.state.packetLostUploadString }
|
||||
@@ -426,6 +434,6 @@ function _mapStateToProps(state, ownProps) {
|
||||
};
|
||||
}
|
||||
|
||||
ConnectionStatusComponent_ = translate(connect(_mapStateToProps)(ConnectionStatusComponent));
|
||||
ConnectionStatusComponent_ = translate(connect(_mapStateToProps)(withTheme(ConnectionStatusComponent)));
|
||||
|
||||
export default ConnectionStatusComponent_;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { isLocalParticipantModerator } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractMuteVideoButton, { _mapStateToProps as _abstractMapStateToProps } from '../AbstractMuteVideoButton';
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state, ownProps),
|
||||
visible: isLocalParticipantModerator(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(AbstractMuteVideoButton));
|
||||
@@ -59,8 +59,10 @@ class PinButton extends AbstractButton<Props, *> {
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { isOpen } = state['features/participants-pane'];
|
||||
|
||||
return {
|
||||
visible: shouldDisplayTileView(state)
|
||||
visible: !isOpen && shouldDisplayTileView(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,29 +2,37 @@
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { BottomSheet, isDialogOpen } from '../../../base/dialog';
|
||||
import { KICK_OUT_ENABLED, getFeatureFlag } from '../../../base/flags';
|
||||
import { getParticipantDisplayName } from '../../../base/participants';
|
||||
import {
|
||||
getParticipantById,
|
||||
getParticipantDisplayName
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
import { PrivateMessageButton } from '../../../chat';
|
||||
import { hideRemoteVideoMenu } from '../../actions.native';
|
||||
import ConnectionStatusButton from '../native/ConnectionStatusButton';
|
||||
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import GrantModeratorButton from './GrantModeratorButton';
|
||||
import KickButton from './KickButton';
|
||||
import MuteButton from './MuteButton';
|
||||
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
|
||||
import MuteVideoButton from './MuteVideoButton';
|
||||
import PinButton from './PinButton';
|
||||
import styles from './styles';
|
||||
|
||||
// import VolumeSlider from './VolumeSlider';
|
||||
|
||||
|
||||
/**
|
||||
* Size of the rendered avatar in the menu.
|
||||
*/
|
||||
const AVATAR_SIZE = 25;
|
||||
const AVATAR_SIZE = 24;
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -63,10 +71,20 @@ type Props = {
|
||||
*/
|
||||
_isOpen: boolean,
|
||||
|
||||
/**
|
||||
* Whether the participant is present in the room or not.
|
||||
*/
|
||||
_isParticipantAvailable?: boolean,
|
||||
|
||||
/**
|
||||
* Display name of the participant retrieved from Redux.
|
||||
*/
|
||||
_participantDisplayName: string
|
||||
_participantDisplayName: string,
|
||||
|
||||
/**
|
||||
* The ID of the participant.
|
||||
*/
|
||||
_participantID: ?string,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
@@ -94,7 +112,13 @@ class RemoteVideoMenu extends PureComponent<Props> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _disableKick, _disableRemoteMute, _disableGrantModerator, participant } = this.props;
|
||||
const {
|
||||
_disableKick,
|
||||
_disableRemoteMute,
|
||||
_disableGrantModerator,
|
||||
_isParticipantAvailable,
|
||||
participant
|
||||
} = this.props;
|
||||
const buttonProps = {
|
||||
afterClick: this._onCancel,
|
||||
showLabel: true,
|
||||
@@ -105,14 +129,19 @@ class RemoteVideoMenu extends PureComponent<Props> {
|
||||
return (
|
||||
<BottomSheet
|
||||
onCancel = { this._onCancel }
|
||||
renderHeader = { this._renderMenuHeader }>
|
||||
renderHeader = { this._renderMenuHeader }
|
||||
showSlidingView = { _isParticipantAvailable }>
|
||||
{ !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
|
||||
<MuteEveryoneElseButton { ...buttonProps } />
|
||||
{ !_disableRemoteMute && <MuteVideoButton { ...buttonProps } /> }
|
||||
<Divider style = { styles.divider } />
|
||||
{ !_disableKick && <KickButton { ...buttonProps } /> }
|
||||
{ !_disableGrantModerator && <GrantModeratorButton { ...buttonProps } /> }
|
||||
<PinButton { ...buttonProps } />
|
||||
<PrivateMessageButton { ...buttonProps } />
|
||||
<MuteEveryoneElseButton { ...buttonProps } />
|
||||
<ConnectionStatusButton { ...buttonProps } />
|
||||
{/* <Divider style = { styles.divider } />*/}
|
||||
{/* <VolumeSlider participantID = { _participantID } />*/}
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
@@ -173,6 +202,7 @@ function _mapStateToProps(state, ownProps) {
|
||||
const kickOutEnabled = getFeatureFlag(state, KICK_OUT_ENABLED, true);
|
||||
const { participant } = ownProps;
|
||||
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
|
||||
const isParticipantAvailable = getParticipantById(state, participant.id);
|
||||
let { disableKick } = remoteVideoMenu;
|
||||
|
||||
disableKick = disableKick || !kickOutEnabled;
|
||||
@@ -182,7 +212,9 @@ function _mapStateToProps(state, ownProps) {
|
||||
_disableKick: Boolean(disableKick),
|
||||
_disableRemoteMute: Boolean(disableRemoteMute),
|
||||
_isOpen: isDialogOpen(state, RemoteVideoMenu_),
|
||||
_participantDisplayName: getParticipantDisplayName(state, participant.id)
|
||||
_isParticipantAvailable: Boolean(isParticipantAvailable),
|
||||
_participantDisplayName: getParticipantDisplayName(state, participant.id),
|
||||
_participantID: participant.id
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
180
react/features/video-menu/components/native/SharedVideoMenu.js
Normal file
180
react/features/video-menu/components/native/SharedVideoMenu.js
Normal file
@@ -0,0 +1,180 @@
|
||||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { BottomSheet, isDialogOpen } from '../../../base/dialog';
|
||||
import {
|
||||
getParticipantById,
|
||||
getParticipantDisplayName
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
import { SharedVideoButton } from '../../../shared-video/components';
|
||||
import { hideSharedVideoMenu } from '../../actions.native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* Size of the rendered avatar in the menu.
|
||||
*/
|
||||
const AVATAR_SIZE = 24;
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The participant for which this menu opened for.
|
||||
*/
|
||||
participant: Object,
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of the BottomSheet.
|
||||
*/
|
||||
_bottomSheetStyles: StyleType,
|
||||
|
||||
/**
|
||||
* True if the menu is currently open, false otherwise.
|
||||
*/
|
||||
_isOpen: boolean,
|
||||
|
||||
/**
|
||||
* Whether the participant is present in the room or not.
|
||||
*/
|
||||
_isParticipantAvailable?: boolean,
|
||||
|
||||
/**
|
||||
* Display name of the participant retrieved from Redux.
|
||||
*/
|
||||
_participantDisplayName: string,
|
||||
|
||||
/**
|
||||
* The ID of the participant.
|
||||
*/
|
||||
_participantID: ?string,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let SharedVideoMenu_;
|
||||
|
||||
/**
|
||||
* Class to implement a popup menu that opens upon long pressing a fake participant thumbnail.
|
||||
*/
|
||||
class SharedVideoMenu extends PureComponent<Props> {
|
||||
/**
|
||||
* Constructor of the component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._renderMenuHeader = this._renderMenuHeader.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_isParticipantAvailable,
|
||||
participant
|
||||
} = this.props;
|
||||
|
||||
const buttonProps = {
|
||||
afterClick: this._onCancel,
|
||||
showLabel: true,
|
||||
participantID: participant.id,
|
||||
styles: this.props._bottomSheetStyles.buttons
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
onCancel = { this._onCancel }
|
||||
renderHeader = { this._renderMenuHeader }
|
||||
showSlidingView = { _isParticipantAvailable }>
|
||||
<Divider style = { styles.divider } />
|
||||
<SharedVideoButton { ...buttonProps } />
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
|
||||
_onCancel: () => boolean;
|
||||
|
||||
/**
|
||||
* Callback to hide the {@code SharedVideoMenu}.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onCancel() {
|
||||
if (this.props._isOpen) {
|
||||
this.props.dispatch(hideSharedVideoMenu());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_renderMenuHeader: () => React$Element<any>;
|
||||
|
||||
/**
|
||||
* Function to render the menu's header.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderMenuHeader() {
|
||||
const { _bottomSheetStyles, participant } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
_bottomSheetStyles.sheet,
|
||||
styles.participantNameContainer ] }>
|
||||
<Avatar
|
||||
participantId = { participant.id }
|
||||
size = { AVATAR_SIZE } />
|
||||
<Text style = { styles.participantNameLabel }>
|
||||
{ this.props._participantDisplayName }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { participant } = ownProps;
|
||||
const isParticipantAvailable = getParticipantById(state, participant.id);
|
||||
|
||||
return {
|
||||
_bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
|
||||
_isOpen: isDialogOpen(state, SharedVideoMenu_),
|
||||
_isParticipantAvailable: Boolean(isParticipantAvailable),
|
||||
_participantDisplayName: getParticipantDisplayName(state, participant.id),
|
||||
_participantID: participant.id
|
||||
};
|
||||
}
|
||||
|
||||
SharedVideoMenu_ = connect(_mapStateToProps)(SharedVideoMenu);
|
||||
|
||||
export default SharedVideoMenu_;
|
||||
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import Slider from '@react-native-community/slider';
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Slider, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import { withTheme } from 'react-native-paper';
|
||||
|
||||
import { Icon, IconVolumeEmpty } from '../../../base/icons';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { setVolume } from '../../../participants-pane/actions.native';
|
||||
import { VOLUME_SLIDER_SCALE } from '../../constants';
|
||||
@@ -101,7 +101,7 @@ class VolumeSlider extends PureComponent<Props, State> {
|
||||
return (
|
||||
<View style = { styles.volumeSliderContainer } >
|
||||
<Icon
|
||||
size = { 20 }
|
||||
size = { 24 }
|
||||
src = { IconVolumeEmpty } />
|
||||
<Slider
|
||||
maximumTrackTintColor = { palette.field02 }
|
||||
@@ -144,12 +144,10 @@ function mapStateToProps(state, ownProps): Object {
|
||||
const { participantID } = ownProps;
|
||||
const { participantsVolume } = state['features/participants-pane'];
|
||||
const { startSilent } = state['features/base/config'];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_startSilent: Boolean(startSilent),
|
||||
_volume: localParticipant ? undefined : participantID
|
||||
? participantsVolume[participantID] : undefined
|
||||
_volume: participantID && participantsVolume[participantID]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,4 +8,5 @@ export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog'
|
||||
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
|
||||
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
|
||||
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
|
||||
export { default as SharedVideoMenu } from './SharedVideoMenu';
|
||||
export { default as VolumeSlider } from './VolumeSlider';
|
||||
|
||||
@@ -11,8 +11,8 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
export default createStyleSheet({
|
||||
participantNameContainer: {
|
||||
alignItems: 'center',
|
||||
borderBottomColor: ColorPalette.lightGrey,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: BaseTheme.palette.dividerColor,
|
||||
borderBottomWidth: 0.4,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
flexDirection: 'row',
|
||||
@@ -29,12 +29,14 @@ export default createStyleSheet({
|
||||
},
|
||||
|
||||
statsTitleText: {
|
||||
color: BaseTheme.palette.text01,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginRight: 3
|
||||
},
|
||||
|
||||
statsInfoText: {
|
||||
color: BaseTheme.palette.text01,
|
||||
fontSize: 16,
|
||||
marginRight: 2,
|
||||
marginLeft: 2
|
||||
@@ -48,18 +50,22 @@ export default createStyleSheet({
|
||||
},
|
||||
|
||||
statsWrapper: {
|
||||
marginVertical: 10
|
||||
margin: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
volumeSliderContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginLeft: BaseTheme.spacing[3],
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
marginHorizontal: BaseTheme.spacing[3],
|
||||
marginVertical: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
sliderContainer: {
|
||||
marginLeft: BaseTheme.spacing[3],
|
||||
minWidth: '84%'
|
||||
minWidth: '80%'
|
||||
},
|
||||
|
||||
divider: {
|
||||
backgroundColor: BaseTheme.palette.dividerColor
|
||||
}
|
||||
});
|
||||
|
||||
@@ -189,9 +189,15 @@ function _updateReceiverVideoConstraints({ getState }) {
|
||||
}
|
||||
const { lastN } = state['features/base/lastn'];
|
||||
const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
|
||||
const { visibleParticipants } = state['features/filmstrip'];
|
||||
const { participantId: largeVideoParticipantId } = state['features/large-video'];
|
||||
const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
|
||||
let { visibleParticipants } = state['features/filmstrip'];
|
||||
|
||||
// TODO: implement this on mobile.
|
||||
if (navigator.product === 'ReactNative') {
|
||||
visibleParticipants = Array.from(state['features/base/participants'].remote.keys());
|
||||
}
|
||||
|
||||
const receiverConstraints = {
|
||||
constraints: {},
|
||||
defaultConstraints: { 'maxHeight': VIDEO_QUALITY_LEVELS.NONE },
|
||||
|
||||
75
resources/load-test/package-lock.json
generated
75
resources/load-test/package-lock.json
generated
@@ -1709,14 +1709,36 @@
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.7.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.3.tgz",
|
||||
"integrity": "sha512-jWvmhqYpx+9EZm/FxcZSbUZyDEvDTLDi3nSAKbzEkyWvtI0mNSmUosey+5awDW1RUlrgXbQb5A6qY1xQH9U6MQ==",
|
||||
"version": "4.16.6",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
|
||||
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001010",
|
||||
"electron-to-chromium": "^1.3.306",
|
||||
"node-releases": "^1.1.40"
|
||||
"caniuse-lite": "^1.0.30001219",
|
||||
"colorette": "^1.2.2",
|
||||
"electron-to-chromium": "^1.3.723",
|
||||
"escalade": "^3.1.1",
|
||||
"node-releases": "^1.1.71"
|
||||
},
|
||||
"dependencies": {
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001228",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz",
|
||||
"integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==",
|
||||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.736",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz",
|
||||
"integrity": "sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig==",
|
||||
"dev": true
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.72",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz",
|
||||
"integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
@@ -1825,12 +1847,6 @@
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001012",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001012.tgz",
|
||||
"integrity": "sha512-7RR4Uh04t9K1uYRWzOJmzplgEOAXbfK72oVNokCdMzA67trrhPzy93ahKk1AWHiA0c58tD2P+NHqxrA8FZ+Trg==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
@@ -2049,6 +2065,12 @@
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
},
|
||||
"colorette": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
|
||||
"dev": true
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
@@ -2426,12 +2448,6 @@
|
||||
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==",
|
||||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.314",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.314.tgz",
|
||||
"integrity": "sha512-IKDR/xCxKFhPts7h+VaSXS02Z1mznP3fli1BbXWXeN89i2gCzKraU8qLpEid8YzKcmZdZD3Mly3cn5/lY9xsBQ==",
|
||||
"dev": true
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
@@ -2552,6 +2568,12 @@
|
||||
"is-symbol": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
|
||||
"dev": true
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -4547,23 +4569,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.41",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.41.tgz",
|
||||
"integrity": "sha512-+IctMa7wIs8Cfsa8iYzeaLTFwv5Y4r5jZud+4AnfymzeEXKBCavFX0KBgzVaPVqf0ywa6PrO8/b+bPqdwjGBSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver": "^6.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
|
||||
@@ -136,6 +136,7 @@ function on_message(event)
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
end
|
||||
room.av_moderation[mediaType] = {};
|
||||
room.av_moderation_actors[mediaType] = occupant.nick;
|
||||
end
|
||||
else
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 71 KiB |
Reference in New Issue
Block a user