Compare commits

..

41 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
1433a1ee5d feat(rn,filmstrip) add 1on1 mode
When there are only 2 participants in a call, don't show the remote thumbnail,
unless the `disable1On1Mode` config option is set or the local participant pin
themselves.
2021-08-06 13:31:00 +02:00
dependabot[bot]
834ee22bc3 chore(deps): bump browserslist in /resources/load-test
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.7.3 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.7.3...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-06 10:02:48 +02:00
Pierre
d6b5687828 fix(misc) fix typeof comparison to undefined 2021-08-06 09:51:54 +02:00
Jaya Allamsetty
6b496d4def chore(deps) lib-jitsi-meet@latest
* fix(ConnectionQuality): Do not show red/yellow GSM bars on join. When the user first unmutes their video, the connection quality is shown as poor until the local stats are available. Calculate the connection quality only after the stats are available, i.e., assume 100% until pcStatsInterval has elapsed.
* feat(non-participant-messages) Add a new JitiConferenceEvent for messages ignored by ENDPOINT_MESSAGE_RECEIVED
* fix(precall) respect custom callstats script url for precall test

9e632a77c5...6a3df11ffa
2021-08-05 17:07:43 -04:00
Jaya Allamsetty
22cc56ce8d chore(deps) lib-jitsi-meet@latest
* feat(BridgeChannel): Signal a new videoType for high fps screenshare. This lets the bridge adjust the bitrate allocation for this source so that layers with higher fps are prioritized over layers with higher resolution. As a result, endpoints with restricted downlink will receive a high fps low resolution share as opposed to a high resolution low fps screenshare.
* fix(log) lower severity of overly verbose logs (2)

fa834c2923...9e632a77c5
2021-08-05 14:16:52 -04:00
Saúl Ibarra Corretgé
0419c5a15b fix(rn,video-quality) fix not selecting any endpoint on mobile
The concept of "visible participants" is not yet implemented.
2021-08-05 17:28:33 +02:00
hmuresan
dda1f3c5ba fix(recording-label) fix recording label behavior 2021-08-05 17:56:12 +03:00
hmuresan
6f41ef75d7 fix(notifications) Fix hiding audio/video lost notification 2021-08-05 16:36:13 +03:00
Saúl Ibarra Corretgé
46cbc0ff49 chore(rn,versions) set mobile app and SDK versions 2021-08-05 13:43:29 +02:00
Saúl Ibarra Corretgé
53a695da90 fix(twa) update masked icon
Contrary to what I thought the expected icon is an icon that can be masked, not
the mask itself.
2021-08-05 13:42:58 +02:00
Calin Chitu
8bbee7d1dc feat(toolbox) updated menu items order, dividers for mobile 2021-08-05 11:56:44 +02:00
Дамян Минков
72d4aa7dd5 fix: Fix av moderation enable-disable sequence.
When you enable and then disable av-moderation just the audio moderation is disabled and video moderation disabling is not signalled to moderated clients.
2021-08-04 19:41:37 +03:00
Saúl Ibarra Corretgé
8161309e28 deps(rn) update WebRTC to M92 2021-08-04 15:42:37 +02:00
Saúl Ibarra Corretgé
465e7f1458 fix(conference) unify conference options
Some options were missing on the mobile side, notably calltsts
enableDisplayNameInStats and enableEmailInStats. Now the same logic will be used
in web and mobile.
2021-08-04 15:32:03 +02:00
Calinteodor
4e43a31ec9 fix(rn,bottom-sheet) fixed surface color
Also fix properly showing the remote menu.
2021-08-04 15:11:32 +02:00
Christoph Settgast
70c5ea04b1 fix(lang) update German translation
Signed-off-by: Christoph Settgast <csett86@web.de>
2021-08-04 11:00:34 +02:00
Calinteodor
ca25be7314 fix(config) comment out enabledReactions 2021-08-04 10:58:07 +02:00
Calinteodor
3c2ad24652 fix(shared-video,video-menu) add ability to stop shared video from video menu
Specifically, in the bottom sheet (on mobile) and participants pane.
2021-08-04 10:51:05 +02:00
Avram Tudor
e421a119e1 feat(share-video) Allow sharing direct video links (mp4 etc) on mobile (#9511)
* feat(share-video) Allow sharing direct video links (mp4 etc) on mobile

* fix linting

* code review
2021-08-02 15:55:52 +03:00
Hristo Terezov
619acaca24 fix(Filmstrip): Send only visible endpoints to jvb 2021-07-30 11:44:16 -05:00
Calin Chitu
bc9f5773fb feat(participants-pane) changed to standard header 2021-07-30 16:07:48 +02:00
Andrei Gavrilescu
d0be8dcf9d fix(external-api): persist audio output device 2021-07-30 14:51:47 +03:00
Alex Bumbu
af9958ad66 feat(ios) support for destroying & reinitializing the react native bridge 2021-07-30 11:53:30 +02:00
Calinteodor
efc5c9dabe feat(participants-pane) hide admit all if less than 2 participants
- Fixed admitMultiple action for mobile
- Added token color for button
- Hide Admit all button if less than 2 knocking participants
2021-07-30 11:48:06 +02:00
Calinteodor
d22fc88ae3 feat(participants-pane) context menu ui fixes
- Fixed background color for all participants context menus
- Removed connection status from ReactVideoMenu and added it for local participants
- Removed AVModeration comments on mobile
- Show on stage option visible only when participants pane is closed
2021-07-30 11:46:49 +02:00
Mihai-Andrei Uscat
9ee75038b6 fix(Toolbar): Fix toolbar not hiding on mobile 2021-07-30 11:37:45 +03:00
Saúl Ibarra Corretgé
09af88088d fix(logging) reduce overly vebose logging
It huurts performance on mobile. It can still be enabled by setting the level to
debug / trace.
2021-07-28 23:27:54 +02:00
Saúl Ibarra Corretgé
2e539ba010 chore(deps) lib-jitsi-meet@latest
* fix(log) lower severity of overly verbose logs
* e2ee: remove legacy apis (#1653)

b815157a22...fa834c2923
2021-07-28 23:14:08 +02:00
José Luís Andrade
87b3ec2cc0 fix(lang) update portuguese translaation 2021-07-28 22:22:10 +02:00
Pawel Domas
907b51925d fix(prejoin): disposed track was added to the conference
It is not 100% clear to me when it happens, but I think it could happen
in some race condition where a track is unmuted when it's being disposed
or something around those lines.

The fact is that any muted tracks are disposed by replaceLocalTrack(track.jitsiTrack, null) and they should not be used anymore.

Supposedly fixes a crash:

Failed to add local track to conference Track has been already disposed
2021-07-28 09:00:54 -05:00
Saúl Ibarra Corretgé
643340c4a6 fix(deps,rn) bump @react-native-async-storage/async-storage
In version 1.15 the storage backend was rewritten, which hopefully allows us to
fix this crash on Android:

Caused by java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase: /data/user/0/org.jitsi.meet/databases/RKStorage
       at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:55)
       at android.database.sqlite.SQLiteDatabase.queryWithFactory(SQLiteDatabase.java:1160)
       at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1036)
       at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1204)
       at com.reactnativecommunity.asyncstorage.AsyncStorageModule$1.doInBackgroundGuarded(AsyncStorageModule.java:159)
       at com.reactnativecommunity.asyncstorage.AsyncStorageModule$1.doInBackgroundGuarded(AsyncStorageModule.java:146)
       at com.facebook.react.bridge.GuardedAsyncTask.doInBackground(GuardedAsyncTask.java:35)
       at com.facebook.react.bridge.GuardedAsyncTask.doInBackground(GuardedAsyncTask.java:19)
       at android.os.AsyncTask$2.call(AsyncTask.java:305)
       at java.util.concurrent.FutureTask.run(FutureTask.java:237)
       at com.reactnativecommunity.asyncstorage.AsyncStorageModule$SerialExecutor$1.run(AsyncStorageModule.java:63)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
       at java.lang.Thread.run(Thread.java:760)
2021-07-28 15:26:00 +02:00
Calinteodor
d6c821d524 feat(participants-pane) updates
- Fixed react native community slider to work on both android and ios
- Removed InviteButton from native menus
- Fixed buttons spacing in native OverflowMenu
- Participant context menu details are shown only for remote participants
2021-07-27 16:08:33 +02:00
Saúl Ibarra Corretgé
eb16f93153 fix(rn,polyfills) remove no longer needed hack
With react-native-webrtc 1.89.2 the remote SDP is properly updated before
onaddstream is fired so it's no longer needed.

Also, for readability, IPv6 address synthesis has been moved to a standalone
utils file.
2021-07-26 16:16:27 +02:00
Saúl Ibarra Corretgé
47576aebba chore(deps) react-native-webrtc@1.89.2
THe new version fixed a longstanding problem with RN not updating the JS side
SDP representation properly. This will allow us to remove a hack we currently
have to sidestep this.
2021-07-26 16:16:27 +02:00
Saúl Ibarra Corretgé
bac0a55421 fix(config) add missing buttons to default constants
- Remove button list from interface_config.js since it has been deprecated for a
  while
- Alphabetically sort buttons in config.js and constants.js to make it easier to
  add / remove items
- Add missing invite and toggle-camera buttons to default constants
- Remove no longer existing "fodeviceselection" button

Fixes: https://github.com/jitsi/jitsi-meet/issues/9605
2021-07-26 15:33:38 +02:00
hmuresan
1c8103c444 fix(dropbox-recording) Prevent start recording when no dropbox token 2021-07-26 16:20:05 +03:00
Mihai-Andrei Uscat
4e83e93eb6 fix(virtual-background): Refactor CSS to accommodate smaller screens 2021-07-26 11:57:39 +03:00
Calin Chitu
0f8fa4f059 feat(participants-pane) removed getIsParticipantAudioMuted 2021-07-22 12:54:16 -05:00
Hristo Terezov
becaf0806a fix(ShareDesktopButton): typo. 2021-07-22 11:32:44 -05:00
hmuresan
5b77d722d7 fix(toolbox) add back toggle camera button 2021-07-22 16:20:45 +03:00
hmuresan
f4cde2192e fix(toolbar-buttons) Attempt fix Meet in integration 2021-07-22 16:18:56 +03:00
95 changed files with 2425 additions and 1758 deletions

View File

@@ -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

View File

@@ -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'
}

View File

@@ -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) {

View File

@@ -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')

View File

@@ -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;
},
/**

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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'

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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];

View File

@@ -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"
}
}

View File

@@ -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",

View File

@@ -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}})",

View File

@@ -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'
};

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -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).

View File

@@ -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'
];

View File

@@ -142,7 +142,9 @@ class BottomSheet extends PureComponent<Props> {
<SafeAreaView
style = { [
styles.sheetItemContainer,
_styles.sheet,
renderHeader
? _styles.sheetHeader
: _styles.sheet,
style,
{
maxHeight: _height - 100

View File

@@ -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
}
});

View File

@@ -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 }

View File

@@ -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.

View File

@@ -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',

View File

@@ -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
};
}

View File

@@ -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;
}

View File

@@ -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));

View File

@@ -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)
};
}

View File

@@ -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,

View File

@@ -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'

View File

@@ -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);
}

View File

@@ -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
});
}

View 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
});
}

View File

@@ -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;

View File

@@ -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 });
}
/**

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>
);
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 />

View File

@@ -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';

View File

@@ -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
}
};

View File

@@ -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 => (

View File

@@ -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
};
}

View File

@@ -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)

View File

@@ -235,7 +235,6 @@ export const ParticipantActionsHover = styled(ParticipantActions)`
position: absolute;
top: 0;
transform: translateX(-100%);
width: 40px;
}
`;

View File

@@ -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';

View File

@@ -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));

View File

@@ -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': {

View File

@@ -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;
}
}

View File

@@ -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
};
}

View 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);

View File

@@ -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);

View 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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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';

View File

@@ -4,7 +4,7 @@
* The style of toolbar buttons.
*/
export default {
youtubeVideoContainer: {
videoContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',

View File

@@ -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
*/

View File

@@ -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;
}

View File

@@ -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.
*

View File

@@ -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.
*

View File

@@ -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'
};

View File

@@ -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>
);

View File

@@ -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 }

View File

@@ -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';
}

View 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));

View File

@@ -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 && (

View File

@@ -27,3 +27,5 @@ export const THRESHOLDS = [
order: [ 'microphone', 'camera' ]
}
];
export const NOT_APPLICABLE = 'N/A';

View File

@@ -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: {

View File

@@ -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));
};
}

View File

@@ -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';

View File

@@ -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';
/**

View File

@@ -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';

View File

@@ -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_;

View File

@@ -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));

View File

@@ -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)
};
}

View File

@@ -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
};
}

View 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_;

View File

@@ -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]
};
}

View File

@@ -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';

View File

@@ -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
}
});

View File

@@ -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 },

View File

@@ -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",

View File

@@ -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