Compare commits

...

29 Commits

Author SHA1 Message Date
Hristo Terezov
45aa8606e6 ref: Use openURLInBrowser whenever possible 2019-10-15 18:50:54 +01:00
Mihai Uscat
8be02f9ca1 Implement review changes 2019-10-15 06:54:54 -07:00
Mihai Uscat
3c25a4c08c Naming conventions; Add variables 2019-10-15 06:54:54 -07:00
Mihai Uscat
5ade0cad8b feat(welcome): add posibility to extend settings toolbar 2019-10-15 06:54:54 -07:00
Saúl Ibarra Corretgé
0fa6ffc439 deps: react-native-google-signin@3.0.1 2019-10-14 19:12:45 +02:00
Saúl Ibarra Corretgé
c2ed296178 android: make check more resilient
If action is null (observed on some old devices) we'll get an exception.
Reversing the check fixes it since Actions.XXX is statically defined.
2019-10-14 17:45:43 +02:00
Saúl Ibarra Corretgé
febd12b871 ci: use Xcode 11.1 on Travis 2019-10-14 16:47:07 +02:00
Hristo Terezov
0a06e256b7 feat(HelpButton): Mobile support. 2019-10-14 07:35:39 -07:00
Hristo Terezov
f295f60bea feat(HelpOverflowButton): Implement. 2019-10-14 07:35:39 -07:00
Saúl Ibarra Corretgé
4a8f787519 rn: evaluate config.js in a sandboxed environment
We are downloading code off the Internet and executing it on the user's device,
so run it sandboxed to avoid potential bad actors.

Since it's impossible to eval() safely in JS and React Native doesn't offer
something akin to Node's vm module, here we are rolling our own.

On Android it uses the Duktape JavaScript engine and on iOS the builtin
JavaScriptCore engine. The extra JS engine is *only* used for evaluating the
downloaded code and returning a JSON string which is then passed back to RN.
2019-10-14 12:20:58 +02:00
Saúl Ibarra Corretgé
d85b869934 rn: skip loading configured scriptUrls
None of them work on mobile.
2019-10-14 12:20:58 +02:00
Saúl Ibarra Corretgé
35130f0736 rn: refactor loadScript
- use AbortController for setting the fetch timeout
- use async / await syntax for clarify
- set the default timeout to 5s (previously non-existent, aka 0)
- add ability to load but not evaluate a script
2019-10-14 12:20:58 +02:00
Saúl Ibarra Corretgé
1feff9709c config: drop configLocation and getroomnode options
They never worked on mobile and pose an impediment for makinf config.js more
future proof. Specially if we want to move to a non-executable form of
configuration.
2019-10-14 12:20:58 +02:00
Leonard Kim
1010f53a84 fix(config): add whitelisting for interface config
For now all keys are whitelisted.
2019-10-11 09:38:56 -07:00
Saúl Ibarra Corretgé
f7a526f488 rn: fix rendering unnecessary stuff when in PiP mode 2019-10-11 17:17:53 +02:00
Bettenbuk Zoltan
245eb89b85 fix BottomSheet shaking 2019-10-11 15:14:51 +02:00
Hristo Terezov
99de9d0bfa fix(remoteVideo): Attaching video stream. 2019-10-11 04:58:01 -07:00
Saúl Ibarra Corretgé
98698ba89a etherpad: refactor to share code with mobile
- simplify initialization procedure
- set user display name as the Etherpad name\
- use SharedDocumentButton
2019-10-10 11:19:38 +02:00
Saúl Ibarra Corretgé
19d1e3829d rn: add shared document support using Etherpad 2019-10-10 11:19:38 +02:00
Saúl Ibarra Corretgé
612586ed1f deps: react-native-webview@7.4.1 2019-10-10 11:19:38 +02:00
Saúl Ibarra Corretgé
2609e43f29 ios: misc Xcode changes due to an update 2019-10-10 11:19:38 +02:00
Saúl Ibarra Corretgé
c5cd4f534c dial-in-summary: center the loading indicator 2019-10-10 11:19:38 +02:00
Bettenbuk Zoltan
6e10ca5dd2 fix: chat error message 2019-10-09 18:35:09 +02:00
Bettenbuk Zoltan
0fff1c3534 ref: serve makefile libs locally 2019-10-09 11:56:06 +02:00
Bettenbuk Zoltan
42271b1b89 feat: private messages 2019-10-08 18:22:45 +02:00
Bettenbuk Zoltan
f270b50972 fix: remove unnecessary escaping 2019-10-08 18:22:45 +02:00
Saúl Ibarra Corretgé
ab4b6be9d7 rn: throw exception if default conference options set the room
See: https://github.com/jitsi/jitsi-meet/issues/4720
2019-10-08 14:31:13 +02:00
Saúl Ibarra Corretgé
c45ee0230f android: add getters to JitsiMeetConferenceOptions 2019-10-08 14:31:13 +02:00
George Politis
d210f2f2e7 Adds a noAutoPlayVideo configuration option (used in testing). (#4714)
This adds an option to disable video autoplay that will be used mostly with maleus (our selenium-based load testing tool for testing the new bridge). Disabling video rendering lowers the resource utilisation of the selenium nodes.
2019-10-08 11:34:25 +02:00
107 changed files with 2548 additions and 910 deletions

View File

@@ -1,4 +1,4 @@
osx_image: xcode10.2
osx_image: xcode11.1
language: objective-c
script:
- "./ios/travis-ci/build-ipa.sh"

View File

@@ -45,6 +45,7 @@ dependencies {
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.squareup.duktape:duktape-android:1.3.0'
if (!rootProject.ext.libreBuild) {
implementation 'com.amplitude:android-sdk:2.14.1'

View File

@@ -0,0 +1,57 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;
import com.squareup.duktape.Duktape;
@ReactModule(name = JavaScriptSandboxModule.NAME)
class JavaScriptSandboxModule extends ReactContextBaseJavaModule {
public static final String NAME = "JavaScriptSandbox";
public JavaScriptSandboxModule(ReactApplicationContext reactContext) {
super(reactContext);
}
/**
* Evaluates the given code in a Duktape VM.
* @param code - The code that needs to evaluated.
* @param promise - Resolved with the output in case of success or rejected with an exception
* in case of failure.
*/
@ReactMethod
public void evaluate(String code, Promise promise) {
Duktape vm = Duktape.create();
try {
Object res = vm.evaluate(code);
promise.resolve(res.toString());
} catch (Throwable tr) {
promise.reject(tr);
} finally {
vm.close();
}
}
@Override
public String getName() {
return NAME;
}
}

View File

@@ -33,6 +33,9 @@ public class JitsiMeet {
}
public static void setDefaultConferenceOptions(JitsiMeetConferenceOptions options) {
if (options != null && options.getRoom() != null) {
throw new RuntimeException("'room' must be null in the default conference options");
}
defaultConferenceOptions = options;
}

View File

@@ -72,6 +72,46 @@ public class JitsiMeetConferenceOptions implements Parcelable {
*/
private JitsiMeetUserInfo userInfo;
public URL getServerURL() {
return serverURL;
}
public String getRoom() {
return room;
}
public String getSubject() {
return subject;
}
public String getToken() {
return token;
}
public Bundle getColorScheme() {
return colorScheme;
}
public Bundle getFeatureFlags() {
return featureFlags;
}
public boolean getAudioMuted() {
return audioMuted;
}
public boolean getAudioOnly() {
return audioOnly;
}
public boolean getVideoMuted() {
return videoMuted;
}
public JitsiMeetUserInfo getUserInfo() {
return userInfo;
}
/**
* Class used to build the immutable {@link JitsiMeetConferenceOptions} object.
*/

View File

@@ -87,7 +87,7 @@ public class JitsiMeetOngoingConferenceService extends Service
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final String action = intent.getAction();
if (action.equals(Actions.START)) {
if (Actions.START.equals(action)) {
Notification notification = OngoingNotification.buildOngoingConferenceNotification();
if (notification == null) {
stopSelf();
@@ -96,7 +96,7 @@ public class JitsiMeetOngoingConferenceService extends Service
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
} else if (action.equals(Actions.HANGUP)) {
} else if (Actions.HANGUP.equals(action)) {
JitsiMeetLogger.i(TAG + " Hangup requested");
// Abort all ongoing calls
if (AudioModeModule.useConnectionService()) {

View File

@@ -67,6 +67,7 @@ class ReactInstanceManagerHolder {
new AudioModeModule(reactContext),
new DropboxModule(reactContext),
new ExternalAPIModule(reactContext),
new JavaScriptSandboxModule(reactContext),
new LocaleDetector(reactContext),
new LogBridgeModule(reactContext),
new PictureInPictureModule(reactContext),

View File

@@ -10,7 +10,7 @@ project(':react-native-community-async-storage').projectDir = new File(rootProje
include ':react-native-community_netinfo'
project(':react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
include ':react-native-google-signin'
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-signin/android')
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/google-signin/android')
include ':react-native-immersive'
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
include ':react-native-keep-awake'

View File

@@ -104,7 +104,6 @@ import {
trackRemoved
} from './react/features/base/tracks';
import { getJitsiMeetGlobalNS } from './react/features/base/util';
import { addMessage } from './react/features/chat';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
import {
@@ -114,7 +113,6 @@ import {
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
import { suspendDetected } from './react/features/power-monitor';
import { setSharedVideoStatus } from './react/features/shared-video';
import { isButtonEnabled } from './react/features/toolbox';
import { endpointMessageReceived } from './react/features/subtitles';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -244,8 +242,6 @@ class ConferenceConnector {
this._handleConferenceJoined.bind(this));
room.on(JitsiConferenceEvents.CONFERENCE_FAILED,
this._onConferenceFailed.bind(this));
room.on(JitsiConferenceEvents.CONFERENCE_ERROR,
this._onConferenceError.bind(this));
}
/**
@@ -348,31 +344,6 @@ class ConferenceConnector {
}
}
/**
*
*/
_onConferenceError(err, ...params) {
logger.error('CONFERENCE Error:', err, params);
switch (err) {
case JitsiConferenceErrors.CHAT_ERROR:
logger.error('Chat error.', err);
if (isButtonEnabled('chat') && !interfaceConfig.filmStripOnly) {
const [ code, msg ] = params;
APP.store.dispatch(addMessage({
hasRead: true,
error: code,
message: msg,
messageType: 'error',
timestamp: Date.now()
}));
}
break;
default:
logger.error('Unknown error.', err);
}
}
/**
*
*/

View File

@@ -1,16 +1,6 @@
/* eslint-disable no-unused-vars, no-var */
var config = {
// Configuration
//
// Alternative location for the configuration.
// configLocation: './config.json',
// Custom function which given the URL path should return a room name.
// getroomnode: function (path) { return 'someprefixpossiblybasedonpath'; },
// Connection
//
@@ -60,6 +50,10 @@ var config = {
// Enables the test specific features consumed by jitsi-meet-torture
// testMode: false
// Disables the auto-play behavior of *all* newly created video element.
// This is useful when the client runs on a host with limited resources.
// noAutoPlayVideo: false
},
// Disables ICE/UDP by filtering out local and remote UDP candidates in
@@ -425,6 +419,10 @@ var config = {
// the menu has option to flip the locally seen video for local presentations
// disableLocalVideoFlip: false
// If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for
// user documentation.
// userDocumentationURL: 'https://docs.example.com/video-meetings.html'
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold

View File

@@ -80,6 +80,27 @@
}
}
#chat-recipient {
align-items: center;
background-color: $defaultWarningColor;
display: flex;
flex-direction: row;
padding: 10px;
span {
color: white;
display: flex;
flex: 1;
}
div {
svg {
cursor: pointer;
fill: white
}
}
}
.chat-header {
background-color: $chatHeaderBackgroundColor;
height: 70px;
@@ -196,6 +217,11 @@
padding: 0;
}
}
.privatemessagenotice {
color: $defaultWarningColor;
font-style: italic;
}
}
.smiley {
@@ -228,6 +254,7 @@
.smileys-panel {
bottom: 100%;
box-sizing: border-box;
background-color: rgba(0, 0, 0, .6) !important;
height: auto;
max-height: 0;
overflow: hidden;
@@ -312,6 +339,16 @@
.chatmessage-wrapper {
max-width: 100%;
.replywrapper {
display: flex;
flex-direction: row;
align-items: center;
.toolbox-icon {
cursor: pointer;
}
}
}
.chatmessage {

View File

@@ -6,7 +6,7 @@
min-width: 75px;
text-align: left;
padding: 0px;
width: 150px;
width: 180px;
white-space: nowrap;
&__item {
@@ -87,6 +87,7 @@
display: inline-block;
min-width: 20px;
height: 100%;
padding-right: 10px;
> * {
@include absoluteAligning();

View File

@@ -28,6 +28,7 @@ $defaultColor: #F1F1F1;
$defaultSideBarFontColor: #44A5FF;
$defaultSemiDarkColor: #ACACAC;
$defaultDarkColor: #2b3d5c;
$defaultWarningColor: rgb(215, 121, 118);
/**
* Toolbar
@@ -163,9 +164,45 @@ $watermarkHeight: 74px;
*/
$welcomePageDescriptionColor: #fff;
$welcomePageFontFamily: inherit;
$welcomePageHeaderBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%);
$welcomePageBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%);
$welcomePageTitleColor: #fff;
$welcomePageHeaderBackground: none;
$welcomePageHeaderBackgroundSmall: none;
$welcomePageHeaderBackgroundPosition: none;
$welcomePageHeaderBackgroundRepeat: none;
$welcomePageHeaderBackgroundSize: none;
$welcomePageHeaderPaddingBottom: 0px;
$welcomePageHeaderTextMarginTop: 35px;
$welcomePageHeaderTextMarginBottom: 35px;
$welcomePageHeaderTextTitleMarginBottom: 16px;
$welcomePageHeaderTextDescriptionDisplay: inherit;
$welcomePageEnterRoomWidth: 680px;
$welcomePageEnterRoomPadding: 25px 30px;
$welcomePageEnterRoomBorderRadius: 0px;
$welcomePageEnterRoomInputContainerPadding: 0 8px 5px 0px;
$welcomePageEnterRoomInputContainerBorderWidth: 0px 0px 2px 0px;
$welcomePageEnterRoomInputContainerBorderStyle: solid;
$welcomePageEnterRoomInputContainerBorderImage: linear-gradient(to right, #dee1e6, #fff) 1;
$welcomePageEnterRoomTitleDisplay: inherit;
$welcomePageTabContainerDisplay: flex;
$welcomePageTabContentDisplay: inherit;
$welcomePageTabButtonsDisplay: flex;
$welcomePageTabDisplay: block;
$welcomePageButtonWidth: 51px;
$welcomePageButtonHeight: 35px;
$welcomePageButtonFontWeight: inherit;
$welcomePageButtonBorderRadius: 4px;
$welcomePageButtonLineHeight: 35px;
/**
* Deep-linking page variables.
*/

View File

@@ -4,7 +4,7 @@ body.welcome-page {
}
.welcome {
background-image: $welcomePageHeaderBackground;
background-image: $welcomePageBackground;
display: flex;
flex-direction: column;
font-family: $welcomePageFontFamily;
@@ -13,6 +13,11 @@ body.welcome-page {
position: relative;
.header {
background-image: $welcomePageHeaderBackground;
background-position: $welcomePageHeaderBackgroundPosition;
background-repeat: $welcomePageHeaderBackgroundRepeat;
background-size: $welcomePageHeaderBackgroundSize;
padding-bottom: $welcomePageHeaderPaddingBottom;
align-items: center;
display: flex;
flex-direction: column;
@@ -24,8 +29,8 @@ body.welcome-page {
.header-text {
display: flex;
flex-direction: column;
margin-top: $watermarkHeight + 35;
margin-bottom: 35px;
margin-top: $watermarkHeight + $welcomePageHeaderTextMarginTop;
margin-bottom: $welcomePageHeaderTextMarginBottom;
max-width: calc(100% - 40px);
width: 650px;
z-index: $zindex2;
@@ -36,10 +41,11 @@ body.welcome-page {
font-size: 2.5rem;
font-weight: 500;
line-height: 1.18;
margin-bottom: 16px;
margin-bottom: $welcomePageHeaderTextTitleMarginBottom;
}
.header-text-description {
display: $welcomePageHeaderTextDescriptionDisplay;
color: $welcomePageDescriptionColor;
font-size: 1rem;
font-weight: 400;
@@ -51,23 +57,24 @@ body.welcome-page {
display: flex;
align-items: center;
max-width: calc(100% - 40px);
width: 680px;
width: $welcomePageEnterRoomWidth;
z-index: $zindex2;
background-color: #fff;
padding: 25px 30px;
padding: $welcomePageEnterRoomPadding;
border-radius: $welcomePageEnterRoomBorderRadius;
.enter-room-input-container {
width: 100%;
padding-right: 8px;
padding-bottom: 5px;
padding: $welcomePageEnterRoomInputContainerPadding;
text-align: left;
color: #253858;
height: fit-content;
border-width: 0px 0px 2px 0px;
border-style: solid;
border-image: linear-gradient(to right, #dee1e6, #fff) 1;
border-width: $welcomePageEnterRoomInputContainerBorderWidth;
border-style: $welcomePageEnterRoomInputContainerBorderStyle;
border-image: $welcomePageEnterRoomInputContainerBorderImage;
.enter-room-title {
display: $welcomePageEnterRoomTitleDisplay;
font-size: 18px;
font-weight: bold;
padding-bottom: 5px;
@@ -94,10 +101,11 @@ body.welcome-page {
min-height: 354px;
width: 710px;
background: #75A7E7;
display: flex;
display: $welcomePageTabContainerDisplay;
flex-direction: column;
.tab-content{
display: $welcomePageTabContentDisplay;
margin: 5px 0px;
overflow: hidden;
flex-grow: 1;
@@ -111,13 +119,14 @@ body.welcome-page {
.tab-buttons {
font-size: 18px;
color: #FFFFFF;
display: flex;
display: $welcomePageTabButtonsDisplay;
flex-grow: 0;
flex-direction: row;
min-height: 54px;
width: 100%;
.tab {
display: $welcomePageTabDisplay;
text-align: center;
background: rgba(9,30,66,0.37);
height: 55px;
@@ -138,15 +147,16 @@ body.welcome-page {
}
.welcome-page-button {
width: 51px;
height: 35px;
width: $welcomePageButtonWidth;
height: $welcomePageButtonHeight;
font-size: 14px;
font-weight: $welcomePageButtonFontWeight;
background: #0074E0;
border-radius: 4px;
border-radius: $welcomePageButtonBorderRadius;
color: #FFFFFF;
text-align: center;
vertical-align: middle;
line-height: 35px;
line-height: $welcomePageButtonLineHeight;
cursor: pointer;
}

View File

@@ -0,0 +1 @@
/** Insert custom CSS for any additional content in the welcome page settings toolbar **/

View File

@@ -51,6 +51,7 @@ $flagsImagePath: "../images/";
@import 'ringing/ringing';
@import 'welcome_page';
@import 'welcome_page_content';
@import 'welcome_page_settings_toolbar';
@import 'toolbars';
@import 'jquery.contextMenu';
@import 'keyboard-shortcuts';

View File

@@ -148,6 +148,7 @@
<!--#include virtual="title.html" -->
<!--#include virtual="plugin.head.html" -->
<!--#include virtual="static/welcomePageAdditionalContent.html" -->
<!--#include virtual="static/settingsToolbarAdditionalContent.html" -->
</head>
<body>
<div id="react"></div>

View File

@@ -27,6 +27,7 @@ var interfaceConfig = {
SHOW_DEEP_LINKING_IMAGE: false,
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
DISPLAY_WELCOME_PAGE_CONTENT: true,
DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT: false,
APP_NAME: 'Jitsi Meet',
NATIVE_APP_NAME: 'Jitsi Meet',
PROVIDER_NAME: 'Jitsi',
@@ -221,6 +222,13 @@ var interfaceConfig = {
* milliseconds, those notifications should remain displayed.
*/
// ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT: 15000,
// List of undocumented settings
/**
INDICATOR_FONT_SIZES
MOBILE_DYNAMIC_LINK
PHONE_NUMBER_REGEX
*/
};
/* eslint-enable no-unused-vars, no-var, max-len */

View File

@@ -62,7 +62,7 @@ target 'JitsiMeet' do
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'BVLinearGradient', :path => '../node_modules/react-native-linear-gradient'
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-community/async-storage'
pod 'RNGoogleSignin', :path => '../node_modules/react-native-google-signin'
pod 'RNGoogleSignin', :path => '../node_modules/@react-native-community/google-signin'
pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNSVG', :path => '../node_modules/react-native-svg'
pod 'RNWatch', :path => '../node_modules/react-native-watch-connectivity'

View File

@@ -1,5 +1,10 @@
PODS:
- Amplitude-iOS (4.0.4)
- AppAuth (1.2.0):
- AppAuth/Core (= 1.2.0)
- AppAuth/ExternalUserAgent (= 1.2.0)
- AppAuth/Core (1.2.0)
- AppAuth/ExternalUserAgent (1.2.0)
- boost-for-react-native (1.63.0)
- BVLinearGradient (2.5.6):
- React
@@ -62,18 +67,10 @@ PODS:
- GoogleUtilities/Network (~> 5.2)
- "GoogleUtilities/NSData+zlib (~> 5.2)"
- nanopb (~> 0.3)
- GoogleSignIn (4.4.0):
- "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
- "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)"
- GoogleSignIn (5.0.1):
- AppAuth (~> 1.2)
- GTMAppAuth (~> 1.0)
- GTMSessionFetcher/Core (~> 1.1)
- GoogleToolboxForMac/DebugUtils (2.2.0):
- GoogleToolboxForMac/Defines (= 2.2.0)
- GoogleToolboxForMac/Defines (2.2.0)
- "GoogleToolboxForMac/NSDictionary+URLArguments (2.2.0)":
- GoogleToolboxForMac/DebugUtils (= 2.2.0)
- GoogleToolboxForMac/Defines (= 2.2.0)
- "GoogleToolboxForMac/NSString+URLArguments (= 2.2.0)"
- "GoogleToolboxForMac/NSString+URLArguments (2.2.0)"
- GoogleUtilities/AppDelegateSwizzler (5.4.1):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
@@ -92,7 +89,14 @@ PODS:
- GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (5.4.1):
- GoogleUtilities/Logger
- GTMSessionFetcher/Core (1.2.1)
- GTMAppAuth (1.0.0):
- AppAuth/Core (~> 1.0)
- GTMSessionFetcher (~> 1.1)
- GTMSessionFetcher (1.2.2):
- GTMSessionFetcher/Full (= 1.2.2)
- GTMSessionFetcher/Core (1.2.2)
- GTMSessionFetcher/Full (1.2.2):
- GTMSessionFetcher/Core (= 1.2.2)
- nanopb (0.3.901):
- nanopb/decode (= 0.3.901)
- nanopb/encode (= 0.3.901)
@@ -272,7 +276,7 @@ PODS:
- React
- react-native-webrtc (1.75.0):
- React
- react-native-webview (5.8.1):
- react-native-webview (7.4.1):
- React
- React-RCTActionSheet (0.61.1):
- React-Core/RCTActionSheetHeaders (= 0.61.1)
@@ -330,8 +334,8 @@ PODS:
- ReactCommon/turbomodule/core (= 0.61.1)
- RNCAsyncStorage (1.3.4):
- React
- RNGoogleSignin (2.0.0):
- GoogleSignIn (~> 4.4.0)
- RNGoogleSignin (3.0.1):
- GoogleSignIn (~> 5.0.0)
- React
- RNSound (0.11.0):
- React
@@ -386,7 +390,7 @@ DEPENDENCIES:
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- ReactCommon/turbomodule (from `../node_modules/react-native/ReactCommon`)
- "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)"
- RNGoogleSignin (from `../node_modules/react-native-google-signin`)
- "RNGoogleSignin (from `../node_modules/@react-native-community/google-signin`)"
- RNSound (from `../node_modules/react-native-sound`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNWatch (from `../node_modules/react-native-watch-connectivity`)
@@ -406,12 +410,14 @@ SPEC REPOS:
- FirebaseDynamicLinks
- FirebaseInstanceID
- GoogleAppMeasurement
- GoogleSignIn
- GoogleToolboxForMac
- GoogleUtilities
- GTMSessionFetcher
- nanopb
- ObjectiveDropboxOfficial
trunk:
- AppAuth
- GoogleSignIn
- GTMAppAuth
- GTMSessionFetcher
EXTERNAL SOURCES:
BVLinearGradient:
@@ -479,7 +485,7 @@ EXTERNAL SOURCES:
RNCAsyncStorage:
:path: "../node_modules/@react-native-community/async-storage"
RNGoogleSignin:
:path: "../node_modules/react-native-google-signin"
:path: "../node_modules/@react-native-community/google-signin"
RNSound:
:path: "../node_modules/react-native-sound"
RNSVG:
@@ -491,6 +497,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Amplitude-iOS: 2ad4d7270c99186236c1272a3a9425463b1ae1a7
AppAuth: bce82c76043657c99d91e7882e8a9e1a93650cd4
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
CocoaLumberjack: 2f44e60eb91c176d471fdba43b9e3eae6a721947
@@ -508,10 +515,10 @@ SPEC CHECKSUMS:
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
GoogleAppMeasurement: 6cf307834da065863f9faf4c0de0a936d81dd832
GoogleSignIn: 7ff245e1a7b26d379099d3243a562f5747e23d39
GoogleToolboxForMac: ff31605b7d66400dcec09bed5861689aebadda4d
GoogleSignIn: 3a51b9bb8e48b635fd7f4272cee06ca260345b86
GoogleUtilities: 1e25823cbf46540b4284f6ef8e17b3a68ee12bbc
GTMSessionFetcher: 32aeca0aa144acea523e1c8e053089dec2cb98ca
GTMAppAuth: 4deac854479704f348309e7b66189e604cf5e01e
GTMSessionFetcher: 61bb0f61a4cb560030f1222021178008a5727a23
nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48
ObjectiveDropboxOfficial: a5afefc83f6467c42c45f2253f583f2ad1ffc701
RCTRequired: 53825815218847d3e9c7b6d92ad2d197a926d51e
@@ -528,7 +535,7 @@ SPEC CHECKSUMS:
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-webrtc: c5e3d631179a933548a8e49bddbd8fad02586095
react-native-webview: a95842e3f351a6d2c8bc8bcc9eab689c7e7e5ad4
react-native-webview: 4dbc1d2a4a6b9c5e9e723c62651917aa2b5e579e
React-RCTActionSheet: af4d951113b1e068bb30611f91b984a7a73597ff
React-RCTAnimation: 4f518d70bb6890b7c3d9d732f84786d6693ca297
React-RCTBlob: 072a4888c08de0eef6d04eaa727d25e577e6ff26
@@ -540,12 +547,12 @@ SPEC CHECKSUMS:
React-RCTVibration: 8be61459e3749d1fb02cf414edd05b3007622882
ReactCommon: 4fba5be89efdf0b5720e0adb3d8d7edf6e532db0
RNCAsyncStorage: 8e31405a9f12fbf42c2bb330e4560bfd79c18323
RNGoogleSignin: d030c6c6591db24c3cee649f64c7babf0a1699a0
RNGoogleSignin: 39336070b35fc4cea6a98cf111e00480317be0ae
RNSound: c980916b596cc15c8dcd2f6ecd3b13c4881dbe20
RNSVG: aac12785382e8fd4f28d072fe640612e34914631
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
Yoga: d8c572ddec8d05b7dba08e4e5f1924004a177078
PODFILE CHECKSUM: 0ac392c47cc78d9db03eea7b58950184336d6cf4
PODFILE CHECKSUM: cb84b325b724c6ef7c8b24aa52ca7b6f681a095c
COCOAPODS: 1.8.1

View File

@@ -41,8 +41,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -52,8 +50,8 @@
ReferencedContainer = "container:app.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -75,8 +73,6 @@
ReferencedContainer = "container:app.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@@ -42,6 +42,7 @@
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0B209A0F660027712B /* JMCallKitListener.swift */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
DE438CDA2350934700DD541D /* JavaScriptSandbox.m in Sources */ = {isa = PBXBuildFile; fileRef = DE438CD82350934700DD541D /* JavaScriptSandbox.m */; };
DE65AACA2317FFCD00290BEC /* LogUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = DE65AAC92317FFCD00290BEC /* LogUtils.h */; };
DE65AACC2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DE65AACB2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h */; };
DE762DB422AFDE76000DEBD6 /* JitsiMeetUserInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -104,6 +105,7 @@
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
DE438CD82350934700DD541D /* JavaScriptSandbox.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JavaScriptSandbox.m; sourceTree = "<group>"; };
DE65AAC92317FFCD00290BEC /* LogUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LogUtils.h; sourceTree = "<group>"; };
DE65AACB2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetBaseLogHandler+Private.h"; sourceTree = "<group>"; };
DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetUserInfo.h; sourceTree = "<group>"; };
@@ -190,6 +192,7 @@
A4A934E7212F3AB8001E9388 /* dropbox */,
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
0BD906E91EC0C00300C8C18E /* Info.plist */,
DE438CD82350934700DD541D /* JavaScriptSandbox.m */,
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */,
DEFE535821FB311F00011A3A /* JitsiMeet+Private.h */,
DEFE535321FB1BF800011A3A /* JitsiMeet.m */,
@@ -493,6 +496,7 @@
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */,
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,
DEFE535421FB1BF800011A3A /* JitsiMeet.m in Sources */,
DE438CDA2350934700DD541D /* JavaScriptSandbox.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -635,11 +639,7 @@
INFOPLIST_FILE = src/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -670,11 +670,7 @@
INFOPLIST_FILE = src/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -0,0 +1,55 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import JavaScriptCore;
#import <React/RCTBridgeModule.h>
@interface JavaScriptSandbox : NSObject<RCTBridgeModule>
@end
@implementation JavaScriptSandbox
RCT_EXPORT_MODULE();
+ (BOOL)requiresMainQueueSetup {
return NO;
}
#pragma mark - Exported methods
RCT_EXPORT_METHOD(evaluate:(NSString *)code
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
__block BOOL hasError = NO;
JSContext *ctx = [[JSContext alloc] init];
ctx.exceptionHandler = ^(JSContext *context, JSValue *exception) {
hasError = YES;
reject(@"evaluate", [exception toString], nil);
};
JSValue *ret = [ctx evaluateScript:code];
if (!hasError) {
NSString *result = [ret toString];
if (result == nil) {
reject(@"evaluate", @"Error in string coercion", nil);
} else {
resolve(result);
}
}
}
@end

View File

@@ -37,7 +37,6 @@
* List of domains used for universal linking.
*/
@property (copy, nonatomic, nullable) NSArray<NSString *> *universalLinkDomains;
/**
* Default conference options used for all conferences. These options will be merged
* with those passed to JitsiMeetView.join when joining a conference.

View File

@@ -90,8 +90,7 @@
if ([RNGoogleSignin application:app
openURL:url
sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]
annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]) {
options:options]) {
return YES;
}
@@ -179,6 +178,15 @@
return _universalLinkDomains ? _universalLinkDomains : @[];
}
- (void)setDefaultConferenceOptions:(JitsiMeetConferenceOptions *)defaultConferenceOptions {
if (defaultConferenceOptions != nil && _defaultConferenceOptions.room != nil) {
@throw [NSException exceptionWithName:@"RuntimeError"
reason:@"'room' must be null in the default conference options"
userInfo:nil];
}
_defaultConferenceOptions = defaultConferenceOptions;
}
#pragma mark - Private API methods
- (NSDictionary *)getDefaultProps {

View File

@@ -46,13 +46,16 @@
"today": "Today"
},
"chat": {
"error": "Error: your message \"{{originalText}}\" was not sent. Reason: {{error}}",
"error": "Error: your message was not sent. Reason: {{error}}",
"messagebox": "Type a message",
"messageTo": "Private message to {{recipient}}",
"nickname": {
"popover": "Choose a nickname",
"title": "Enter a nickname to use chat"
},
"title": "Chat"
"privateNotice": "Private message to {{recipient}}",
"title": "Chat",
"you": "you"
},
"connectingOverlay": {
"joiningRoom": "Connecting you to your meeting..."
@@ -235,6 +238,10 @@
"screenSharingFirefoxPermissionDeniedError": "Something went wrong while we were trying to share your screen. Please make sure that you have given us permission to do so. ",
"screenSharingFirefoxPermissionDeniedTitle": "Oops! We werent able to start screen sharing!",
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing extension permissions. Please reload and try again.",
"sendPrivateMessage": "You recently received a private message. Did you intend to reply to that privately, or you want to send your message to the group?",
"sendPrivateMessageCancel": "Send to the group",
"sendPrivateMessageOk": "Send privately",
"sendPrivateMessageTitle": "Send privately?",
"serviceUnavailable": "Service unavailable",
"sessTerminated": "Call terminated",
"Share": "Share",
@@ -268,6 +275,9 @@
"dialOut": {
"statusMessage": "is now {{status}}"
},
"documentSharing" : {
"title": "Shared Document"
},
"feedback": {
"average": "Average",
"bad": "Bad",
@@ -563,6 +573,7 @@
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
"hangup": "Leave the call",
"help": "Help",
"invite": "Invite people",
"kick": "Kick participant",
"localRecording": "Toggle local recording controls",
@@ -571,6 +582,7 @@
"moreActionsMenu": "More actions menu",
"mute": "Toggle mute audio",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Toggle raise hand",
"recording": "Toggle recording",
@@ -603,6 +615,7 @@
"exitTileView": "Exit tile view",
"feedback": "Leave feedback",
"hangup": "Leave",
"help": "Help",
"invite": "Invite people",
"login": "Login",
"logout": "Logout",
@@ -611,6 +624,7 @@
"mute": "Mute / Unmute",
"openChat": "Open chat",
"pip": "Enter Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand",
"raiseYourHand": "Raise your hand",

View File

@@ -15,7 +15,7 @@ import Filmstrip from './videolayout/Filmstrip';
import { getLocalParticipant } from '../../react/features/base/participants';
import { toggleChat } from '../../react/features/chat';
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
import { setDocumentUrl } from '../../react/features/etherpad';
import { setFilmstripVisible } from '../../react/features/filmstrip';
import { setNotificationsEnabled } from '../../react/features/notifications';
import {
@@ -240,10 +240,12 @@ UI.initEtherpad = name => {
return;
}
logger.log('Etherpad is enabled');
etherpadManager
= new EtherpadManager(config.etherpad_base, name, eventEmitter);
APP.store.dispatch(setEtherpadHasInitialzied());
etherpadManager = new EtherpadManager(eventEmitter);
const url = new URL(name, config.etherpad_base);
APP.store.dispatch(setDocumentUrl(url.toString()));
};
/**

View File

@@ -1,22 +1,12 @@
/* global $, APP, interfaceConfig */
import { setDocumentEditingState } from '../../../react/features/etherpad';
import { getSharedDocumentUrl, setDocumentEditingState } from '../../../react/features/etherpad';
import { getToolboxHeight } from '../../../react/features/toolbox';
import VideoLayout from '../videolayout/VideoLayout';
import LargeContainer from '../videolayout/LargeContainer';
import Filmstrip from '../videolayout/Filmstrip';
/**
* Etherpad options.
*/
const options = $.param({
showControls: true,
showChat: false,
showLineNumbers: true,
useMonospaceFont: false
});
/**
*
*/
@@ -70,13 +60,13 @@ class Etherpad extends LargeContainer {
/**
* Creates new Etherpad object
*/
constructor(domain, name) {
constructor(url) {
super();
const iframe = document.createElement('iframe');
iframe.id = 'etherpadIFrame';
iframe.src = `${domain + name}?${options}`;
iframe.src = url;
iframe.frameBorder = 0;
iframe.scrolling = 'no';
iframe.width = DEFAULT_WIDTH;
@@ -199,13 +189,7 @@ export default class EtherpadManager {
/**
*
*/
constructor(domain, name, eventEmitter) {
if (!domain || !name) {
throw new Error('missing domain or name');
}
this.domain = domain;
this.name = name;
constructor(eventEmitter) {
this.eventEmitter = eventEmitter;
this.etherpad = null;
}
@@ -228,7 +212,7 @@ export default class EtherpadManager {
* Create new Etherpad frame.
*/
openEtherpad() {
this.etherpad = new Etherpad(this.domain, this.name);
this.etherpad = new Etherpad(getSharedDocumentUrl(APP.store.getState));
VideoLayout.addLargeVideoContainer(
ETHERPAD_CONTAINER_TYPE,
this.etherpad

View File

@@ -273,7 +273,7 @@ LocalVideo.prototype._updateVideoElement = function() {
// case video does not autoplay.
const video = this.container.querySelector('video');
video && video.play();
video && !config.testing?.noAutoPlayVideo && video.play();
};
export default LocalVideo;

View File

@@ -7,6 +7,7 @@ import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import { createThumbnailOffsetParentIsNullEvent, sendAnalytics } from '../../../react/features/analytics';
import { i18next } from '../../../react/features/base/i18n';
import {
JitsiParticipantConnectionStatus
@@ -512,11 +513,31 @@ RemoteVideo.prototype.addRemoteStreamElement = function(stream) {
$(streamElement).hide();
// If the container is currently visible
// we attach the stream to the element.
if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
this.waitForPlayback(streamElement, stream);
stream.attach(streamElement);
this.waitForPlayback(streamElement, stream);
stream.attach(streamElement);
// TODO: Remove once we verify that this.container.offsetParent === null was the reason for not attached video
// streams to the thumbnail.
if (isVideo && this.container.offsetParent === null) {
sendAnalytics(createThumbnailOffsetParentIsNullEvent(this.id));
const parentNodesDisplayProps = [
'#filmstripRemoteVideosContainer',
'#filmstripRemoteVideos',
'#remoteVideos',
'.filmstrip',
'#videospace',
'#videoconference_page',
'#react'
].map(selector => `${selector} - ${$(selector).css('display')}`);
const videoConferencePageParent = $('#videoconference_page').parent();
const reactDiv = document.getElementById('react');
parentNodesDisplayProps.push(
`${videoConferencePageParent.attr('class')} - ${videoConferencePageParent.css('display')}`);
parentNodesDisplayProps.push(`this.container - ${this.$container.css('display')}`);
logger.debug(`this.container.offsetParent is null [user: ${this.id}, ${
parentNodesDisplayProps.join(', ')}, #react.offsetParent - ${
reactDiv && reactDiv.offsetParent !== null ? 'not null' : 'null'}]`);
}
if (!isVideo) {

View File

@@ -199,7 +199,7 @@ SmallVideo.createStreamElement = function(stream) {
element.muted = true;
}
element.autoplay = true;
element.autoplay = !config.testing?.noAutoPlayVideo;
element.id = SmallVideo.getStreamElementID(stream);
return element;

56
package-lock.json generated
View File

@@ -3062,6 +3062,11 @@
}
}
},
"@react-native-community/google-signin": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/google-signin/-/google-signin-3.0.1.tgz",
"integrity": "sha512-RC9c7ATGdq5IKFqw/h4d8eVTDve8FZxMtsarBHKfP09SrQfEgvOebzVr7YNC+4qs7dFqR+0E2vD7nle1s/dQ3A=="
},
"@react-native-community/netinfo": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.1.5.tgz",
@@ -11681,8 +11686,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#16e08408aa3fa8303e236b9ffdb0dfce65e79bb7",
"from": "github:jitsi/lib-jitsi-meet#16e08408aa3fa8303e236b9ffdb0dfce65e79bb7",
"version": "github:jitsi/lib-jitsi-meet#f9808adb8eb523bae3318f9f8ef49b544651485f",
"from": "github:jitsi/lib-jitsi-meet#f9808adb8eb523bae3318f9f8ef49b544651485f",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.2",
@@ -15441,11 +15446,6 @@
"jssha": "^2.2.0"
}
},
"react-native-google-signin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-2.0.0.tgz",
"integrity": "sha512-9loM4lcCIdbco5BnmNio7yGaXQKCpCaY1VRmYiTSvC5NjuSf6Ui6jZRee46p/YdaU4yRnS3u5Vct6Psrvr0HNg=="
},
"react-native-immersive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-immersive/-/react-native-immersive-2.0.0.tgz",
@@ -15520,14 +15520,19 @@
}
},
"react-native-webview": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-5.8.1.tgz",
"integrity": "sha512-b6pSvmjoiWtcz6YspggW02X+BRXJWuquHwkh37BRx1NMW1iwMZA31SnFQvTpPzWYYIb9WF/mRsy2nGtt9C6NIg==",
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-7.4.1.tgz",
"integrity": "sha512-AVT5HIEEWc/NZdNwXRVev0cAs1Si0O3BA4Crqyor8JbwuxUUCllLv+NK7TO3eOw/ENl+QyIPHu9dizkoycmgJQ==",
"requires": {
"escape-string-regexp": "1.0.5",
"escape-string-regexp": "2.0.0",
"invariant": "2.2.4"
},
"dependencies": {
"escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -16417,35 +16422,6 @@
"version": "github:jitsi/rnnoise-wasm#db96d11f175a22ef56c7db1ba9550835b716e615",
"from": "github:jitsi/rnnoise-wasm#db96d11f175a22ef56c7db1ba9550835b716e615"
},
"rollup-plugin-visualizer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-1.1.1.tgz",
"integrity": "sha512-7xkSKp+dyJmSC7jg2LXqViaHuOnF1VvIFCnsZEKjrgT5ZVyiLLSbeszxFcQSfNJILphqgAEmWAUz0Z4xYScrRw==",
"optional": true,
"requires": {
"mkdirp": "^0.5.1",
"opn": "^5.4.0",
"source-map": "^0.7.3",
"typeface-oswald": "0.0.54"
},
"dependencies": {
"opn": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
"integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==",
"optional": true,
"requires": {
"is-wsl": "^1.1.0"
}
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"optional": true
}
}
},
"rsvp": {
"version": "4.8.5",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",

View File

@@ -35,6 +35,7 @@
"@atlaskit/tooltip": "12.1.13",
"@microsoft/microsoft-graph-client": "1.1.0",
"@react-native-community/async-storage": "1.3.4",
"@react-native-community/google-signin": "3.0.1",
"@react-native-community/netinfo": "4.1.5",
"@svgr/webpack": "4.3.2",
"@tensorflow-models/body-pix": "1.1.2",
@@ -56,7 +57,7 @@
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#16e08408aa3fa8303e236b9ffdb0dfce65e79bb7",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f9808adb8eb523bae3318f9f8ef49b544651485f",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",
@@ -71,7 +72,6 @@
"react-native-background-timer": "2.1.1",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#902e6e92d6bae450a6052f76ba4d02f977ffd8f2",
"react-native-callstats": "3.61.0",
"react-native-google-signin": "2.0.0",
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",
"react-native-linear-gradient": "2.5.6",
@@ -81,7 +81,7 @@
"react-native-swipeout": "2.3.6",
"react-native-watch-connectivity": "0.2.0",
"react-native-webrtc": "github:jitsi/react-native-webrtc#047b019a7ce1ec93ab4a2f6796e997d7a02e8e5d",
"react-native-webview": "5.8.1",
"react-native-webview": "7.4.1",
"react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0",
"react-transition-group": "2.4.0",

View File

@@ -692,6 +692,21 @@ export function createSyncTrackStateEvent(mediaType, muted) {
};
}
/**
* Creates an event that indicates the thumbnail offset parent is null.
*
* @param {string} id - The id of the user related to the thumbnail.
* @returns {Object} The event in a format suitable for sending via sendAnalytics.
*/
export function createThumbnailOffsetParentIsNullEvent(id) {
return {
action: 'OffsetParentIsNull',
attributes: {
id
}
};
}
/**
* Creates an event associated with a toolbar button being clicked/pressed. By
* convention, where appropriate an attribute named 'enable' should be used to

View File

@@ -77,6 +77,11 @@ declare var APP: Object;
* @returns {void}
*/
function _addConferenceListeners(conference, dispatch) {
// A simple logger for conference errors received through
// the listener. These errors are not handled now, but logged.
conference.on(JitsiConferenceEvents.CONFERENCE_ERROR,
error => logger.error('Conference error.', error));
// Dispatches into features/base/conference follow:
conference.on(

View File

@@ -0,0 +1,137 @@
/**
* The config keys to whitelist, the keys that can be overridden.
* Currently we can only whitelist the first part of the properties, like
* 'p2p.useStunTurn' and 'p2p.enabled' we whitelist all p2p options.
* The whitelist is used only for config.js.
*
* @type Array
*/
export default [
'_desktopSharingSourceDevice',
'_peerConnStatusOutOfLastNTimeout',
'_peerConnStatusRtcMuteTimeout',
'abTesting',
'analytics.disabled',
'autoRecord',
'autoRecordToken',
'avgRtpStatsN',
'callFlowsEnabled',
'callStatsConfIDNamespace',
'callStatsID',
'callStatsSecret',
/**
* The display name of the CallKit call representing the conference/meeting
* associated with this config.js including while the call is ongoing in the
* UI presented by CallKit and in the system-wide call history. The property
* is meant for use cases in which the room name is not desirable as a
* display name for CallKit purposes and the desired display name is not
* provided in the form of a JWT callee. As the value is associated with a
* conference/meeting, the value makes sense not as a deployment-wide
* configuration, only as a runtime configuration override/overwrite
* provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callDisplayName',
/**
* The handle
* ({@link https://developer.apple.com/documentation/callkit/cxhandle}) of
* the CallKit call representing the conference/meeting associated with this
* config.js. The property is meant for use cases in which the room URL is
* not desirable as the handle for CallKit purposes. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callHandle',
/**
* The UUID of the CallKit call representing the conference/meeting
* associated with this config.js. The property is meant for use cases in
* which Jitsi Meet is to work with a CallKit call created outside of Jitsi
* Meet and to be adopted by Jitsi Meet such as, for example, an incoming
* and/or outgoing CallKit call created by Jitsi Meet SDK for iOS
* clients/consumers prior to giving control to Jitsi Meet. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callUUID',
'channelLastN',
'constraints',
'debug',
'debugAudioLevels',
'defaultLanguage',
'desktopSharingChromeDisabled',
'desktopSharingChromeExtId',
'desktopSharingChromeMinExtVersion',
'desktopSharingChromeSources',
'desktopSharingFrameRate',
'desktopSharingFirefoxDisabled',
'desktopSharingSources',
'disable1On1Mode',
'disableAEC',
'disableAGC',
'disableAP',
'disableAudioLevels',
'disableDeepLinking',
'disableH264',
'disableHPF',
'disableNS',
'disableRemoteControl',
'disableRtx',
'disableSuspendVideo',
'displayJids',
'e2eping',
'enableDisplayNameInStats',
'enableLayerSuspension',
'enableLipSync',
'disableLocalVideoFlip',
'enableRemb',
'enableStatsID',
'enableTalkWhileMuted',
'enableTcc',
'etherpad_base',
'failICE',
'fileRecordingsEnabled',
'firefox_fake_device',
'forceJVB121Ratio',
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hosts',
'iAmRecorder',
'iAmSipGateway',
'iceTransportPolicy',
'ignoreStartMuted',
'liveStreamingEnabled',
'localRecording',
'minParticipants',
'nick',
'openBridgeChannel',
'p2p',
'preferH264',
'requireDisplayName',
'resolution',
'startAudioMuted',
'startAudioOnly',
'startBitrate',
'startSilent',
'startScreenSharing',
'startVideoMuted',
'startWithVideoMuted',
'subject',
'testing',
'useIPv6',
'useNicks',
'useStunTurn',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable'
];

View File

@@ -2,152 +2,12 @@
import _ from 'lodash';
import CONFIG_WHITELIST from './configWhitelist';
import { _CONFIG_STORE_PREFIX } from './constants';
import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
import parseURLParams from './parseURLParams';
import logger from './logger';
declare var $: Object;
/**
* The config keys to whitelist, the keys that can be overridden.
* Currently we can only whitelist the first part of the properties, like
* 'p2p.useStunTurn' and 'p2p.enabled' we whitelist all p2p options.
* The whitelist is used only for config.js.
*
* @private
* @type Array
*/
const WHITELISTED_KEYS = [
'_desktopSharingSourceDevice',
'_peerConnStatusOutOfLastNTimeout',
'_peerConnStatusRtcMuteTimeout',
'abTesting',
'analytics.disabled',
'autoRecord',
'autoRecordToken',
'avgRtpStatsN',
'callFlowsEnabled',
'callStatsConfIDNamespace',
'callStatsID',
'callStatsSecret',
/**
* The display name of the CallKit call representing the conference/meeting
* associated with this config.js including while the call is ongoing in the
* UI presented by CallKit and in the system-wide call history. The property
* is meant for use cases in which the room name is not desirable as a
* display name for CallKit purposes and the desired display name is not
* provided in the form of a JWT callee. As the value is associated with a
* conference/meeting, the value makes sense not as a deployment-wide
* configuration, only as a runtime configuration override/overwrite
* provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callDisplayName',
/**
* The handle
* ({@link https://developer.apple.com/documentation/callkit/cxhandle}) of
* the CallKit call representing the conference/meeting associated with this
* config.js. The property is meant for use cases in which the room URL is
* not desirable as the handle for CallKit purposes. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callHandle',
/**
* The UUID of the CallKit call representing the conference/meeting
* associated with this config.js. The property is meant for use cases in
* which Jitsi Meet is to work with a CallKit call created outside of Jitsi
* Meet and to be adopted by Jitsi Meet such as, for example, an incoming
* and/or outgoing CallKit call created by Jitsi Meet SDK for iOS
* clients/consumers prior to giving control to Jitsi Meet. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callUUID',
'channelLastN',
'constraints',
'debug',
'debugAudioLevels',
'defaultLanguage',
'desktopSharingChromeDisabled',
'desktopSharingChromeExtId',
'desktopSharingChromeMinExtVersion',
'desktopSharingChromeSources',
'desktopSharingFrameRate',
'desktopSharingFirefoxDisabled',
'desktopSharingSources',
'disable1On1Mode',
'disableAEC',
'disableAGC',
'disableAP',
'disableAudioLevels',
'disableDeepLinking',
'disableH264',
'disableHPF',
'disableNS',
'disableRemoteControl',
'disableRtx',
'disableSuspendVideo',
'displayJids',
'e2eping',
'enableDisplayNameInStats',
'enableLayerSuspension',
'enableLipSync',
'disableLocalVideoFlip',
'enableRemb',
'enableStatsID',
'enableTalkWhileMuted',
'enableTcc',
'etherpad_base',
'failICE',
'fileRecordingsEnabled',
'firefox_fake_device',
'forceJVB121Ratio',
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hosts',
'iAmRecorder',
'iAmSipGateway',
'iceTransportPolicy',
'ignoreStartMuted',
'liveStreamingEnabled',
'localRecording',
'minParticipants',
'nick',
'openBridgeChannel',
'p2p',
'preferH264',
'requireDisplayName',
'resolution',
'startAudioMuted',
'startAudioOnly',
'startBitrate',
'startSilent',
'startScreenSharing',
'startVideoMuted',
'startWithAudioMuted',
'startWithVideoMuted',
'subject',
'testing',
'useIPv6',
'useNicks',
'useStunTurn',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable'
];
// XXX The functions getRoomName and parseURLParams are split out of
// functions.js because they are bundled in both app.bundle and
// do_external_connect, webpack 1 does not support tree shaking, and we don't
@@ -178,69 +38,6 @@ export function createFakeConfig(baseURL: string) {
};
}
/**
* Promise wrapper on obtain config method. When HttpConfigFetch will be moved
* to React app it's better to use load config instead.
*
* @param {string} location - URL of the domain from which the config is to be
* obtained.
* @param {string} room - Room name.
* @private
* @returns {Promise<void>}
*/
export function obtainConfig(location: string, room: string): Promise<void> {
return new Promise((resolve, reject) =>
_obtainConfig(location, room, (success, error) => {
success ? resolve() : reject(error);
})
);
}
/**
* Sends HTTP POST request to specified {@code endpoint}. In request the name
* of the room is included in JSON format:
* {
* "rooomName": "someroom12345"
* }.
*
* @param {string} endpoint - The name of HTTP endpoint to which to send
* the HTTP POST request.
* @param {string} roomName - The name of the conference room for which config
* is requested.
* @param {Function} complete - The callback to invoke upon success or failure.
* @returns {void}
*/
function _obtainConfig(endpoint: string, roomName: string, complete: Function) {
logger.info(`Send config request to ${endpoint} for room: ${roomName}`);
$.ajax(
endpoint,
{
contentType: 'application/json',
data: JSON.stringify({ roomName }),
dataType: 'json',
method: 'POST',
error(jqXHR, textStatus, errorThrown) {
logger.error('Get config error: ', jqXHR, errorThrown);
complete(false, `Get config response status: ${textStatus}`);
},
success(data) {
const { config, interfaceConfig, loggingConfig } = window;
try {
overrideConfigJSON(
config, interfaceConfig, loggingConfig,
data);
complete(true);
} catch (e) {
logger.error('Parse config error: ', e);
complete(false, e);
}
}
}
);
}
/* eslint-disable max-params, no-shadow */
/**
@@ -306,8 +103,8 @@ export function overrideConfigJSON(
/* eslint-enable max-params, no-shadow */
/**
* Whitelist only config.js, skips this for others configs
* (interfaceConfig, loggingConfig).
* Apply whitelist filtering for configs with whitelists, skips this for others
* configs (loggingConfig).
* Only extracts overridden values for keys we allow to be overridden.
*
* @param {string} configName - The config name, one of config,
@@ -318,11 +115,13 @@ export function overrideConfigJSON(
* that are whitelisted.
*/
function _getWhitelistedJSON(configName, configJSON) {
if (configName !== 'config') {
return configJSON;
if (configName === 'interfaceConfig') {
return _.pick(configJSON, INTERFACE_CONFIG_WHITELIST);
} else if (configName === 'config') {
return _.pick(configJSON, CONFIG_WHITELIST);
}
return _.pick(configJSON, WHITELISTED_KEYS);
return configJSON;
}
/**

View File

@@ -11,8 +11,8 @@ export * from './functions.any';
* @returns {void}
*/
export function _cleanupConfig(config: Object) {
config.analytics.scriptURLs = [];
if (NativeModules.AppInfo.LIBRE_BUILD) {
config.analytics.scriptURLs = [];
delete config.analytics.amplitudeAPPKey;
delete config.analytics.googleAnalyticsTrackingId;
delete config.callStatsID;

View File

@@ -1,29 +1,17 @@
/* @flow */
// @flow
import { getBackendSafeRoomName } from '../util';
declare var config: Object;
/**
* Builds and returns the room name.
*
* @returns {string}
*/
export default function getRoomName(): ?string {
const { getroomnode } = config;
const path = window.location.pathname;
let roomName;
// Determine the room node from the URL.
if (getroomnode && typeof getroomnode === 'function') {
roomName = getroomnode.call(config, path);
} else {
// Fall back to the default strategy of making assumptions about how the
// URL maps to the room (name). It currently assumes a deployment in
// which the last non-directory component of the path (name) is the
// room.
roomName = path.substring(path.lastIndexOf('/') + 1) || undefined;
}
// The last non-directory component of the path (name) is the room.
const roomName = path.substring(path.lastIndexOf('/') + 1) || undefined;
return getBackendSafeRoomName(roomName);
}

View File

@@ -0,0 +1,70 @@
/**
* The interface config keys to whitelist, the keys that can be overridden.
*
* @private
* @type Array
*/
export default [
'ANDROID_APP_PACKAGE',
'APP_NAME',
'APP_SCHEME',
'AUDIO_LEVEL_PRIMARY_COLOR',
'AUDIO_LEVEL_SECONDARY_COLOR',
'AUTHENTICATION_ENABLE',
'AUTO_PIN_LATEST_SCREEN_SHARE',
'BRAND_WATERMARK_LINK',
'CLOSE_PAGE_GUEST_HINT',
'CONNECTION_INDICATOR_AUTO_HIDE_ENABLED',
'CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT',
'CONNECTION_INDICATOR_DISABLED',
'DEFAULT_BACKGROUND',
'DEFAULT_LOCAL_DISPLAY_NAME',
'DEFAULT_REMOTE_DISPLAY_NAME',
'DISABLE_DOMINANT_SPEAKER_INDICATOR',
'DISABLE_FOCUS_INDICATOR',
'DISABLE_RINGING',
'DISABLE_TRANSCRIPTION_SUBTITLES',
'DISABLE_VIDEO_BACKGROUND',
'DISPLAY_WELCOME_PAGE_CONTENT',
'ENABLE_FEEDBACK_ANIMATION',
'ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT',
'FILM_STRIP_MAX_HEIGHT',
'GENERATE_ROOMNAMES_ON_WELCOME_PAGE',
'INDICATOR_FONT_SIZES',
'INITIAL_TOOLBAR_TIMEOUT',
'INVITATION_POWERED_BY',
'JITSI_WATERMARK_LINK',
'LANG_DETECTION',
'LIVE_STREAMING_HELP_LINK',
'LOCAL_THUMBNAIL_RATIO',
'MAXIMUM_ZOOMING_COEFFICIENT',
'MOBILE_APP_PROMO',
'MOBILE_DOWNLOAD_LINK_ANDROID',
'MOBILE_DOWNLOAD_LINK_IOS',
'MOBILE_DYNAMIC_LINK',
'NATIVE_APP_NAME',
'OPTIMAL_BROWSERS',
'PHONE_NUMBER_REGEX',
'POLICY_LOGO',
'PROVIDER_NAME',
'RANDOM_AVATAR_URL_PREFIX',
'RANDOM_AVATAR_URL_SUFFIX',
'RECENT_LIST_ENABLED',
'REMOTE_THUMBNAIL_RATIO',
'SETTINGS_SECTIONS',
'SHOW_BRAND_WATERMARK',
'SHOW_DEEP_LINKING_IMAGE',
'SHOW_JITSI_WATERMARK',
'SHOW_POWERED_BY',
'SHOW_WATERMARK_FOR_GUESTS',
'SUPPORT_URL',
'TILE_VIEW_MAX_COLUMNS',
'TOOLBAR_ALWAYS_VISIBLE',
'TOOLBAR_BUTTONS',
'TOOLBAR_TIMEOUT',
'UNSUPPORTED_BROWSERS',
'VERTICAL_FILMSTRIP',
'VIDEO_LAYOUT_FIT',
'VIDEO_QUALITY_LABEL_DISABLED',
'filmStripOnly'
];

View File

@@ -61,11 +61,7 @@ class BottomSheet extends PureComponent<Props> {
styles.sheetItemContainer,
_styles.sheet
] }>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }>
{ this._getWrappedContent() }
</ScrollView>
{ this._getWrappedContent() }
</View>
</View>
</SlidingView>
@@ -73,24 +69,32 @@ class BottomSheet extends PureComponent<Props> {
}
/**
* Wraps the content when needed (iOS 11 and above), or just returns the original children.
* Wraps the content when needed (iOS 11 and above), or just returns the original content.
*
* @returns {React$Element}
*/
_getWrappedContent() {
const content = (
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false } >
{ this.props.children }
</ScrollView>
);
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS > 10) {
return (
<SafeAreaView>
{ this.props.children }
{ content }
</SafeAreaView>
);
}
}
return this.props.children;
return content;
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>

After

Width:  |  Height:  |  Size: 381 B

View File

@@ -28,6 +28,7 @@ export { default as IconExitFullScreen } from './exit-full-screen.svg';
export { default as IconFeedback } from './feedback.svg';
export { default as IconFullScreen } from './full-screen.svg';
export { default as IconHangup } from './hangup.svg';
export { default as IconHelp } from './help.svg';
export { default as IconInfo } from './info.svg';
export { default as IconInvite } from './invite.svg';
export { default as IconKick } from './kick.svg';
@@ -36,6 +37,7 @@ export { default as IconMenu } from './menu.svg';
export { default as IconMenuDown } from './menu-down.svg';
export { default as IconMenuThumb } from './thumb-menu.svg';
export { default as IconMenuUp } from './menu-up.svg';
export { default as IconMessage } from './message.svg';
export { default as IconMicDisabled } from './mic-disabled.svg';
export { default as IconMicrophone } from './microphone.svg';
export { default as IconModerator } from './star.svg';
@@ -48,6 +50,7 @@ export { default as IconRaisedHand } from './raised-hand.svg';
export { default as IconRec } from './rec.svg';
export { default as IconRemoteControlStart } from './play.svg';
export { default as IconRemoteControlStop } from './stop.svg';
export { default as IconReply } from './reply.svg';
export { default as IconRestore } from './restore.svg';
export { default as IconRoomLock } from './security.svg';
export { default as IconRoomUnlock } from './security-locked.svg';

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 194 B

View File

@@ -1,12 +1,7 @@
// @flow
import { toState } from '../redux';
import { loadScript } from '../util';
import JitsiMeetJS from './_';
import logger from './logger';
declare var APP: Object;
const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
@@ -97,42 +92,3 @@ export function isFatalJitsiConnectionError(error: Object | string) {
|| error === JitsiConnectionErrors.OTHER_ERROR
|| error === JitsiConnectionErrors.SERVER_ERROR);
}
/**
* Loads config.js from a specific remote server.
*
* @param {string} url - The URL to load.
* @returns {Promise<Object>}
*/
export function loadConfig(url: string): Promise<Object> {
let promise;
if (typeof APP === 'undefined') {
promise
= loadScript(url, 2.5 * 1000 /* Timeout in ms */)
.then(() => {
const { config } = window;
// We don't want to pollute the global scope.
window.config = undefined;
if (typeof config !== 'object') {
throw new Error('window.config is not an object');
}
return config;
})
.catch(err => {
logger.error(`Failed to load config from ${url}`, err);
throw err;
});
} else {
// Return "the config.js file" from the global scope - that is how the
// Web app on both the client and the server was implemented before the
// React Native app was even conceived.
promise = Promise.resolve(window.config);
}
return promise;
}

View File

@@ -0,0 +1,36 @@
// @flow
import { NativeModules } from 'react-native';
import { loadScript } from '../util';
import logger from './logger';
export * from './functions.any';
const { JavaScriptSandbox } = NativeModules;
/**
* Loads config.js from a specific remote server.
*
* @param {string} url - The URL to load.
* @returns {Promise<Object>}
*/
export async function loadConfig(url: string): Promise<Object> {
try {
const configTxt = await loadScript(url, 2.5 * 1000 /* Timeout in ms */, true /* skipeval */);
const configJson = await JavaScriptSandbox.evaluate(`${configTxt}\nJSON.stringify(config);`);
const config = JSON.parse(configJson);
if (typeof config !== 'object') {
throw new Error('config is not an object');
}
logger.info(`Config loaded from ${url}`);
return config;
} catch (err) {
logger.error(`Failed to load config from ${url}`, err);
throw err;
}
}

View File

@@ -0,0 +1,16 @@
// @flow
export * from './functions.any';
/**
* Loads config.js from a specific remote server.
*
* @param {string} url - The URL to load.
* @returns {Promise<Object>}
*/
export async function loadConfig(url: string): Promise<Object> { // eslint-disable-line no-unused-vars
// Return "the config.js file" from the global scope - that is how the
// Web app on both the client and the server was implemented before the
// React Native app was even conceived.
return window.config;
}

View File

@@ -26,7 +26,13 @@ type Props = {
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object
videoTrack: ?Object,
/**
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
autoPlay: boolean
};
/**
@@ -44,7 +50,7 @@ class Video extends Component<Props> {
*/
static defaultProps = {
className: '',
autoPlay: true,
id: ''
};
@@ -131,7 +137,7 @@ class Video extends Component<Props> {
render() {
return (
<video
autoPlay = { true }
autoPlay = { this.props.autoPlay }
className = { this.props.className }
id = { this.props.id }
ref = { this._setVideoElement } />

View File

@@ -23,7 +23,14 @@ type Props = AbstractVideoTrackProps & {
* The value of the id attribute of the video. Used by the torture tests
* to locate video elements.
*/
id: string
id: string,
/**
*
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
_noAutoPlayVideo: boolean
};
/**
@@ -53,7 +60,7 @@ class VideoTrack extends AbstractVideoTrack<Props> {
render() {
return (
<Video
autoPlay = { true }
autoPlay = { !this.props._noAutoPlayVideo }
className = { this.props.className }
id = { this.props.id }
onVideoPlaying = { this._onVideoPlaying }
@@ -64,4 +71,22 @@ class VideoTrack extends AbstractVideoTrack<Props> {
_onVideoPlaying: () => void;
}
export default connect()(VideoTrack);
/**
* Maps (parts of) the Redux state to the associated VideoTracks props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _noAutoPlayVideo: boolean
* }}
*/
function _mapStateToProps(state) {
const testingConfig = state['features/base/config'].testing;
return {
_noAutoPlayVideo: testingConfig?.noAutoPlayVideo
};
}
export default connect(_mapStateToProps)(VideoTrack);

View File

@@ -28,6 +28,7 @@ export const ColorPalette = {
overflowMenuItemUnderlay: '#EEEEEE',
red: '#D00000',
transparent: 'rgba(0, 0, 0, 0)',
warning: 'rgb(215, 121, 118)',
white: '#FFFFFF',
/**

View File

@@ -1,4 +1,5 @@
export * from './helpers';
export * from './httpUtils';
export * from './loadScript';
export * from './openURLInBrowser';
export * from './uri';

View File

@@ -1,6 +1,9 @@
// @flow
import { timeoutPromise } from './timeoutPromise';
/**
* Default timeout for loading scripts.
*/
const DEFAULT_TIMEOUT = 5000;
/**
* Loads a script from a specific URL. React Native cannot load a JS
@@ -13,63 +16,49 @@ import { timeoutPromise } from './timeoutPromise';
* @param {number} [timeout] - The timeout in millisecnods after which the
* loading of the specified {@code url} is to be aborted/rejected (if not
* settled yet).
* @param {boolean} skipEval - Wether we want to skip evaluating the loaded content or not.
* @returns {void}
*/
export function loadScript(url: string, timeout: ?number): Promise<void> {
return new Promise((resolve, reject) => {
// XXX The implementation of fetch on Android will throw an Exception on
// the Java side which will break the app if the URL is invalid (which
// the implementation of fetch on Android calls 'unexpected url'). In
// order to try to prevent the breakage of the app, try to fail on an
// invalid URL as soon as possible.
const { hostname, pathname, protocol } = new URL(url);
export async function loadScript(
url: string, timeout: number = DEFAULT_TIMEOUT, skipEval: boolean = false): Promise<any> {
// XXX The implementation of fetch on Android will throw an Exception on
// the Java side which will break the app if the URL is invalid (which
// the implementation of fetch on Android calls 'unexpected url'). In
// order to try to prevent the breakage of the app, try to fail on an
// invalid URL as soon as possible.
const { hostname, pathname, protocol } = new URL(url);
// XXX The standard URL implementation should throw an Error if the
// specified URL is relative. Unfortunately, the polyfill used on
// react-native does not.
if (!hostname || !pathname || !protocol) {
reject(`unexpected url: ${url}`);
// XXX The standard URL implementation should throw an Error if the
// specified URL is relative. Unfortunately, the polyfill used on
// react-native does not.
if (!hostname || !pathname || !protocol) {
throw new Error(`unexpected url: ${url}`);
}
return;
const controller = new AbortController();
const signal = controller.signal;
const timer = setTimeout(() => {
controller.abort();
}, timeout);
const response = await fetch(url, { signal });
// If the timeout hits the above will raise AbortError.
clearTimeout(timer);
switch (response.status) {
case 200: {
const txt = await response.text();
if (skipEval) {
return txt;
}
let fetch_ = fetch(url, { method: 'GET' });
// The implementation of fetch provided by react-native is based on
// XMLHttpRequest. Which defines timeout as an unsigned long with
// default value 0, which means there is no timeout.
if (timeout) {
// FIXME I don't like the approach with timeoutPromise because:
//
// * It merely abandons the underlying XHR and, consequently, opens
// us to potential issues with NetworkActivityIndicator which
// tracks XHRs.
//
// * @paweldomas also reported that timeouts seem to be respected by
// the XHR implementation on iOS. Given that we have
// implementation of loadScript based on fetch and XHR (in an
// earlier revision), I don't see why we're not using an XHR
// directly on iOS.
//
// * The approach of timeoutPromise I found on the Internet is to
// directly use XHR instead of fetch and abort the XHR on timeout.
// Which may deal with the NetworkActivityIndicator at least.
fetch_ = timeoutPromise(fetch_, timeout);
}
fetch_
.then(response => {
switch (response.status) {
case 200:
return response.responseText || response.text();
default:
throw response.statusText;
}
})
.then(responseText => {
eval.call(window, responseText); // eslint-disable-line no-eval
})
.then(resolve, reject);
});
return eval.call(window, txt); // eslint-disable-line no-eval
}
default:
throw new Error(`loadScript error: ${response.statusText}`);
}
}

View File

@@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/util');

View File

@@ -0,0 +1,17 @@
// @flow
import { Linking } from 'react-native';
import logger from './logger';
/**
* Opens URL in the browser.
*
* @param {string} url - The URL to be opened.
* @returns {void}
*/
export function openURLInBrowser(url: string) {
Linking.openURL(url).catch(error => {
logger.error(`An error occurred while trying to open ${url}`, error);
});
}

View File

@@ -0,0 +1,11 @@
// @flow
/**
* Opens URL in the browser.
*
* @param {string} url - The URL to be opened.
* @returns {void}
*/
export function openURLInBrowser(url: string) {
window.open(url, '', 'noopener');
}

View File

@@ -30,11 +30,23 @@ export const CLEAR_MESSAGES = 'CLEAR_MESSAGES';
*
* {
* type: SEND_MESSAGE,
* ignorePrivacy: boolean,
* message: string
* }
*/
export const SEND_MESSAGE = 'SEND_MESSAGE';
/**
* The type of action which signals the initiation of sending of as private message to the
* supplied recipient.
*
* {
* participant: Participant,
* type: SET_PRIVATE_MESSAGE_RECIPIENT
* }
*/
export const SET_PRIVATE_MESSAGE_RECIPIENT = 'SET_PRIVATE_MESSAGE_RECIPIENT';
/**
* The type of the action which signals to toggle the display of the chat panel.
*

View File

@@ -4,6 +4,7 @@ import {
ADD_MESSAGE,
CLEAR_MESSAGES,
SEND_MESSAGE,
SET_PRIVATE_MESSAGE_RECIPIENT,
TOGGLE_CHAT
} from './actionTypes';
@@ -53,18 +54,37 @@ export function clearMessages() {
* Sends a chat message to everyone in the conference.
*
* @param {string} message - The chat message to send out.
* @param {boolean} ignorePrivacy - True if the privacy notification should be ignored.
* @returns {{
* type: SEND_MESSAGE,
* ignorePrivacy: boolean,
* message: string
* }}
*/
export function sendMessage(message: string) {
export function sendMessage(message: string, ignorePrivacy: boolean = false) {
return {
type: SEND_MESSAGE,
ignorePrivacy,
message
};
}
/**
* Initiates the sending of a private message to the supplied participant.
*
* @param {Participant} participant - The participant to set the recipient to.
* @returns {{
* participant: Participant,
* type: SET_PRIVATE_MESSAGE_RECIPIENT
* }}
*/
export function setPrivateMessageRecipient(participant: Object) {
return {
participant,
type: SET_PRIVATE_MESSAGE_RECIPIENT
};
}
/**
* Toggles display of the chat side panel.
*

View File

@@ -56,4 +56,32 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
.format(TIMESTAMP_FORMAT);
}
/**
* Generates the message text to be redered in the component.
*
* @returns {string}
*/
_getMessageText() {
const { message } = this.props;
return message.messageType === 'error'
? this.props.t('chat.error', {
error: message.message
})
: message.message;
}
/**
* Returns the message that is displayed as a notice for private messages.
*
* @returns {string}
*/
_getPrivateNoticeMessage() {
const { message, t } = this.props;
return t('chat.privateNotice', {
recipient: message.messageType === 'local' ? message.recipient : t('chat.you')
});
}
}

View File

@@ -0,0 +1,116 @@
// @flow
import { PureComponent } from 'react';
import { sendMessage, setPrivateMessageRecipient } from '../actions';
import { getParticipantById } from '../../base/participants';
type Props = {
/**
* The message that is about to be sent.
*/
message: Object,
/**
* The ID of the participant that we think the message may be intended to.
*/
participantID: string,
/**
* Function to be used to translate i18n keys.
*/
t: Function,
/**
* Prop to be invoked on sending the message.
*/
_onSendMessage: Function,
/**
* Prop to be invoked when the user wants to set a private recipient.
*/
_onSetMessageRecipient: Function,
/**
* The participant retreived from Redux by the participanrID prop.
*/
_participant: Object
};
/**
* Abstract class for the dialog displayed to avoid mis-sending private messages.
*/
export class AbstractChatPrivacyDialog extends PureComponent<Props> {
/**
* Instantiates a new instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSendGroupMessage = this._onSendGroupMessage.bind(this);
this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
}
_onSendGroupMessage: () => boolean;
/**
* Callback to be invoked for cancel action (user wants to send a group message).
*
* @returns {boolean}
*/
_onSendGroupMessage() {
this.props._onSendMessage(this.props.message);
return true;
}
_onSendPrivateMessage: () => boolean;
/**
* Callback to be invoked for submit action (user wants to send a private message).
*
* @returns {void}
*/
_onSendPrivateMessage() {
const { message, _onSendMessage, _onSetMessageRecipient, _participant } = this.props;
_onSetMessageRecipient(_participant);
_onSendMessage(message);
return true;
}
}
/**
* Maps part of the props of this component to Redux actions.
*
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Props}
*/
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
return {
_onSendMessage: (message: Object) => {
dispatch(sendMessage(message, true));
},
_onSetMessageRecipient: participant => {
dispatch(setPrivateMessageRecipient(participant));
}
};
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
return {
_participant: getParticipantById(state, ownProps.participantID)
};
}

View File

@@ -0,0 +1,61 @@
// @flow
import { PureComponent } from 'react';
import { getParticipantDisplayName } from '../../base/participants';
import { setPrivateMessageRecipient } from '../actions';
type Props = {
/**
* Function used to translate i18n labels.
*/
t: Function,
/**
* Function to remove the recipent setting of the chat window.
*/
_onRemovePrivateMessageRecipient: Function,
/**
* The name of the message recipient, if any.
*/
_privateMessageRecipient: ?string
};
/**
* Abstract class for the {@code MessageRecipient} component.
*/
export default class AbstractMessageRecipient extends PureComponent<Props> {
}
/**
* Maps part of the props of this component to Redux actions.
*
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Props}
*/
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
return {
_onRemovePrivateMessageRecipient: () => {
dispatch(setPrivateMessageRecipient());
}
};
}
/**
* 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 { privateMessageRecipient } = state['features/chat'];
return {
_privateMessageRecipient:
privateMessageRecipient ? getParticipantDisplayName(state, privateMessageRecipient.id) : undefined
};
}

View File

@@ -0,0 +1,101 @@
// @flow
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox';
import { translate } from '../../base/i18n';
import { IconMessage, IconReply } from '../../base/icons';
import { getParticipantById } from '../../base/participants';
import { connect } from '../../base/redux';
import { setPrivateMessageRecipient } from '../actions';
export type Props = AbstractButtonProps & {
/**
* The ID of the participant that the message is to be sent.
*/
participantID: string,
/**
* True if the button is rendered as a reply button.
*/
reply: boolean,
/**
* Function to be used to translate i18n labels.
*/
t: Function,
/**
* The participant object retreived from Redux.
*/
_participant: Object,
/**
* Function to dispatch the result of the participant selection to send a private message.
*/
_setPrivateMessageRecipient: Function
};
/**
* Class to render a button that initiates the sending of a private message through chet.
*/
class PrivateMessageButton extends AbstractButton<Props, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage';
icon = IconMessage;
label = 'toolbar.privateMessage';
toggledIcon = IconReply;
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { _participant, _setPrivateMessageRecipient } = this.props;
_setPrivateMessageRecipient(_participant);
}
/**
* Helper function to be implemented by subclasses, which must return a
* {@code boolean} value indicating if this button is toggled or not.
*
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props.reply;
}
}
/**
* Maps part of the props of this component to Redux actions.
*
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Props}
*/
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
return {
_setPrivateMessageRecipient: participant => {
dispatch(setPrivateMessageRecipient(participant));
}
};
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
return {
_participant: getParticipantById(state, ownProps.participantID)
};
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageButton));

View File

@@ -1,3 +1,4 @@
// @flow
export * from './native';
export { default as PrivateMessageButton } from './PrivateMessageButton';

View File

@@ -1,3 +1,5 @@
// @flow
export * from './web';
export { default as PrivateMessageButton } from './PrivateMessageButton';

View File

@@ -16,6 +16,7 @@ import AbstractChat, {
import ChatInputBar from './ChatInputBar';
import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
import styles from './styles';
/**
@@ -53,6 +54,7 @@ class Chat extends AbstractChat<Props> {
onPressBack = { this._onClose } />
<SafeAreaView style = { styles.backdrop }>
<MessageContainer messages = { this.props._messages } />
<MessageRecipient />
<ChatInputBar onSend = { this.props._onSendMessage } />
</SafeAreaView>
</KeyboardAvoidingView>

View File

@@ -10,6 +10,7 @@ import { Linkify } from '../../../base/react';
import { replaceNonUnicodeEmojis } from '../../functions';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
import PrivateMessageButton from '../PrivateMessageButton';
import styles from './styles';
@@ -46,25 +47,30 @@ class ChatMessage extends AbstractChatMessage<Props> {
textWrapperStyle.push(styles.systemTextWrapper);
}
const messageText = message.messageType === 'error'
? this.props.t('chat.error', {
error: message.error,
originalText: message.message
})
: message.message;
return (
<View style = { styles.messageWrapper } >
{ this._renderAvatar() }
<View style = { detailsWrapperStyle }>
<View style = { textWrapperStyle } >
{
this.props.showDisplayName
&& this._renderDisplayName()
}
<Linkify linkStyle = { styles.chatLink }>
{ replaceNonUnicodeEmojis(messageText) }
</Linkify>
<View style = { styles.replyWrapper }>
<View style = { textWrapperStyle } >
{
this.props.showDisplayName
&& this._renderDisplayName()
}
<Linkify linkStyle = { styles.chatLink }>
{ replaceNonUnicodeEmojis(this._getMessageText()) }
</Linkify>
{
message.privateMessage
&& this._renderPrivateNotice()
}
</View>
{ message.privateMessage && !localMessage
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false }
toggledStyles = { styles.replyStyles } /> }
</View>
{ this.props.showTimestamp && this._renderTimestamp() }
</View>
@@ -74,6 +80,10 @@ class ChatMessage extends AbstractChatMessage<Props> {
_getFormattedTimestamp: () => string;
_getMessageText: () => string;
_getPrivateNoticeMessage: () => string;
/**
* Renders the avatar of the sender.
*
@@ -106,6 +116,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
);
}
/**
* Renders the message privacy notice.
*
* @returns {React$Element<*>}
*/
_renderPrivateNotice() {
return (
<Text style = { styles.privateNotice }>
{ this._getPrivateNoticeMessage() }
</Text>
);
}
/**
* Renders the time at which the message was sent.
*

View File

@@ -0,0 +1,37 @@
// @flow
import React from 'react';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { AbstractChatPrivacyDialog, _mapDispatchToProps, _mapStateToProps } from '../AbstractChatPrivacyDialog';
/**
* Implements a component for the dialog displayed to avoid mis-sending private messages.
*/
class ChatPrivacyDialog extends AbstractChatPrivacyDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<ConfirmDialog
cancelKey = 'dialog.sendPrivateMessageCancel'
contentKey = 'dialog.sendPrivateMessage'
okKey = 'dialog.sendPrivateMessageOk'
onCancel = { this._onSendGroupMessage }
onSubmit = { this._onSendPrivateMessage } />
);
}
_onSendGroupMessage: () => boolean;
_onSendPrivateMessage: () => boolean;
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPrivacyDialog));

View File

@@ -0,0 +1,52 @@
// @flow
import React from 'react';
import { Text, TouchableHighlight, View } from 'react-native';
import { translate } from '../../../base/i18n';
import { Icon, IconCancelSelection } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
} from '../AbstractMessageRecipient';
import styles from './styles';
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _privateMessageRecipient } = this.props;
if (!_privateMessageRecipient) {
return null;
}
const { t } = this.props;
return (
<View style = { styles.messageRecipientContainer }>
<Text style = { styles.messageRecipientText }>
{ t('chat.messageTo', {
recipient: _privateMessageRecipient
}) }
</Text>
<TouchableHighlight onPress = { this.props._onRemovePrivateMessageRecipient }>
<Icon
src = { IconCancelSelection }
style = { styles.messageRecipientCancelIcon } />
</TouchableHighlight>
</View>
);
}
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));

View File

@@ -2,3 +2,4 @@
export { default as Chat } from './Chat';
export { default as ChatButton } from './ChatButton';
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';

View File

@@ -80,6 +80,23 @@ export default {
flex: 1
},
messageRecipientCancelIcon: {
color: ColorPalette.white,
fontSize: 18
},
messageRecipientContainer: {
alignItems: 'center',
backgroundColor: ColorPalette.warning,
flexDirection: 'row',
padding: BoxModel.padding
},
messageRecipientText: {
color: ColorPalette.white,
flex: 1
},
/**
* The message text itself.
*/
@@ -115,6 +132,25 @@ export default {
borderTopRightRadius: 0
},
replyWrapper: {
alignItems: 'center',
flexDirection: 'row'
},
replyStyles: {
iconStyle: {
color: 'rgb(118, 136, 152)',
fontSize: 22,
margin: BoxModel.margin / 2
}
},
privateNotice: {
color: ColorPalette.warning,
fontSize: 13,
fontStyle: 'italic'
},
sendButtonIcon: {
color: ColorPalette.darkGrey,
fontSize: 22

View File

@@ -14,6 +14,7 @@ import AbstractChat, {
import ChatInput from './ChatInput';
import DisplayNameForm from './DisplayNameForm';
import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
/**
* React Component for holding the chat feature in a side panel that slides in
@@ -116,7 +117,10 @@ class Chat extends AbstractChat<Props> {
<MessageContainer
messages = { this.props._messages }
ref = { this._messageContainerRef } />
<ChatInput onResize = { this._onChatInputResize } />
<MessageRecipient />
<ChatInput
onResize = { this._onChatInputResize }
onSend = { this.props._onSendMessage } />
</>
);
}

View File

@@ -8,8 +8,6 @@ import type { Dispatch } from 'redux';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { sendMessage } from '../../actions';
import SmileysPanel from './SmileysPanel';
/**
@@ -28,6 +26,11 @@ type Props = {
*/
onResize: ?Function,
/**
* Callback to invoke on message send.
*/
onSend: Function,
/**
* Invoked to obtain translated strings.
*/
@@ -163,7 +166,7 @@ class ChatInput extends Component<Props, State> {
const trimmed = this.state.message.trim();
if (trimmed) {
this.props.dispatch(sendMessage(trimmed));
this.props.onSend(trimmed);
this.setState({ message: '' });
}

View File

@@ -10,6 +10,7 @@ import { Linkify } from '../../../base/react';
import AbstractChatMessage, {
type Props
} from '../AbstractChatMessage';
import PrivateMessageButton from '../PrivateMessageButton';
/**
* Renders a single chat message.
@@ -23,23 +24,10 @@ class ChatMessage extends AbstractChatMessage<Props> {
*/
render() {
const { message } = this.props;
const messageToDisplay = message.messageType === 'error'
? this.props.t('chat.error', {
error: message.error,
originalText: message.message
})
: message.message;
// replace links and smileys
// Strophe already escapes special symbols on sending,
// so we escape here only tags to avoid double &amp;
const escMessage = messageToDisplay.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br/>');
const processedMessage = [];
// content is an array of text and emoji components
const content = toArray(escMessage, { className: 'smiley' });
const content = toArray(this._getMessageText(), { className: 'smiley' });
content.forEach(i => {
if (typeof i === 'string') {
@@ -51,11 +39,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
return (
<div className = 'chatmessage-wrapper'>
<div className = 'chatmessage'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
{ processedMessage }
<div className = 'replywrapper'>
<div className = 'chatmessage'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
{ processedMessage }
</div>
{ message.privateMessage && this._renderPrivateNotice() }
</div>
{ message.privateMessage && message.messageType !== 'local'
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false } /> }
</div>
{ this.props.showTimestamp && this._renderTimestamp() }
</div>
@@ -64,6 +60,10 @@ class ChatMessage extends AbstractChatMessage<Props> {
_getFormattedTimestamp: () => string;
_getMessageText: () => string;
_getPrivateNoticeMessage: () => string;
/**
* Renders the display name of the sender.
*
@@ -77,6 +77,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
);
}
/**
* Renders the message privacy notice.
*
* @returns {React$Element<*>}
*/
_renderPrivateNotice() {
return (
<div className = 'privatemessagenotice'>
{ this._getPrivateNoticeMessage() }
</div>
);
}
/**
* Renders the time at which the message was sent.
*

View File

@@ -0,0 +1,42 @@
/* @flow */
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { AbstractChatPrivacyDialog, _mapDispatchToProps, _mapStateToProps } from '../AbstractChatPrivacyDialog';
/**
* Implements a component for the dialog displayed to avoid mis-sending private messages.
*/
class ChatPrivacyDialog extends AbstractChatPrivacyDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
cancelKey = 'dialog.sendPrivateMessageCancel'
okKey = 'dialog.sendPrivateMessageOk'
onCancel = { this._onSendGroupMessage }
onSubmit = { this._onSendPrivateMessage }
titleKey = 'dialog.sendPrivateMessageTitle'
width = 'small'>
<div>
{ this.props.t('dialog.sendPrivateMessage') }
</div>
</Dialog>
);
}
_onSendGroupMessage: () => boolean;
_onSendPrivateMessage: () => boolean;
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPrivacyDialog));

View File

@@ -0,0 +1,48 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconCancelSelection } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
} from '../AbstractMessageRecipient';
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _privateMessageRecipient } = this.props;
if (!_privateMessageRecipient) {
return null;
}
const { t } = this.props;
return (
<div id = 'chat-recipient'>
<span>
{ t('chat.messageTo', {
recipient: _privateMessageRecipient
}) }
</span>
<div onClick = { this.props._onRemovePrivateMessageRecipient }>
<Icon
src = { IconCancelSelection } />
</div>
</div>
);
}
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));

View File

@@ -2,3 +2,4 @@
export { default as Chat } from './Chat';
export { default as ChatCounter } from './ChatCounter';
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';

View File

@@ -1,14 +1,17 @@
// @flow
import UIUtil from '../../../modules/UI/util/UIUtil';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import {
CONFERENCE_JOINED,
getCurrentConference
} from '../base/conference';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { openDialog } from '../base/dialog';
import {
JitsiConferenceErrors,
JitsiConferenceEvents
} from '../base/lib-jitsi-meet';
import {
getLocalParticipant,
getParticipantById,
getParticipantDisplayName
} from '../base/participants';
@@ -16,14 +19,23 @@ import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { isButtonEnabled, showToolbox } from '../toolbox';
import { SEND_MESSAGE } from './actionTypes';
import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
import { addMessage, clearMessages, toggleChat } from './actions';
import { ChatPrivacyDialog } from './components';
import { INCOMING_MSG_SOUND_ID } from './constants';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
declare var APP: Object;
declare var interfaceConfig : Object;
/**
* Timeout for when to show the privacy notice after a private message was received.
*
* E.g. if this value is 20 secs (20000ms), then we show the privacy notice when sending a non private
* message after we have received a private message in the last 20 seconds.
*/
const PRIVACY_NOTICE_TIMEOUT = 20 * 1000;
/**
* Implements the middleware of the chat feature.
*
@@ -31,14 +43,16 @@ declare var interfaceConfig : Object;
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const { dispatch } = store;
switch (action.type) {
case APP_WILL_MOUNT:
store.dispatch(
dispatch(
registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
break;
case APP_WILL_UNMOUNT:
store.dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
break;
case CONFERENCE_JOINED:
@@ -46,18 +60,43 @@ MiddlewareRegistry.register(store => next => action => {
break;
case SEND_MESSAGE: {
const { conference } = store.getState()['features/base/conference'];
const state = store.getState();
const { conference } = state['features/base/conference'];
if (conference) {
const escapedMessage = UIUtil.escapeHtml(action.message);
// There may be cases when we intend to send a private message but we forget to set the
// recipient. This logic tries to mitigate this risk.
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
if (typeof APP !== 'undefined') {
APP.API.notifySendingChatMessage(escapedMessage);
if (shouldSendPrivateMessageTo) {
dispatch(openDialog(ChatPrivacyDialog, {
message: action.message,
participantID: shouldSendPrivateMessageTo
}));
} else {
// Sending the message if privacy notice doesn't need to be shown.
if (typeof APP !== 'undefined') {
APP.API.notifySendingChatMessage(action.message);
}
const { privateMessageRecipient } = state['features/chat'];
if (privateMessageRecipient) {
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message);
_persistSentPrivateMessage(store, privateMessageRecipient.id, action.message);
} else {
conference.sendTextMessage(action.message);
}
}
conference.sendTextMessage(escapedMessage);
}
break;
}
case SET_PRIVATE_MESSAGE_RECIPIENT: {
_maybeFocusField();
break;
}
}
return next(action);
@@ -103,10 +142,10 @@ StateListenerRegistry.register(
* @private
* @returns {void}
*/
function _addChatMsgListener(conference, { dispatch, getState }) {
function _addChatMsgListener(conference, store) {
if ((typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly)
|| (typeof APP !== 'undefined' && !isButtonEnabled('chat'))
|| getState()['features/base/config'].iAmRecorder) {
|| store.getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if we're in filmStripOnly mode, or
// the chat button is not enabled in interfaceConfig.
// or we are in iAmRecorder mode
@@ -116,44 +155,199 @@ function _addChatMsgListener(conference, { dispatch, getState }) {
conference.on(
JitsiConferenceEvents.MESSAGE_RECEIVED,
(id, message, timestamp, nick) => {
// Logic for all platforms:
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
if (!isChatOpen) {
dispatch(playSound(INCOMING_MSG_SOUND_ID));
}
// Provide a default for for the case when a message is being
// backfilled for a participant that has left the conference.
const participant = getParticipantById(state, id) || {};
const displayName = participant.name || nick || getParticipantDisplayName(state, id);
const hasRead = participant.local || isChatOpen;
const timestampToDate = timestamp
? new Date(timestamp) : new Date();
const millisecondsTimestamp = timestampToDate.getTime();
dispatch(addMessage({
displayName,
hasRead,
_handleReceivedMessage(store, {
id,
messageType: participant.local ? 'local' : 'remote',
message,
timestamp: millisecondsTimestamp
}));
if (typeof APP !== 'undefined') {
// Logic for web only:
APP.API.notifyReceivedChatMessage({
body: message,
id,
nick: displayName,
ts: timestamp
});
dispatch(showToolbox(4000));
}
nick,
privateMessage: false,
timestamp
});
}
);
conference.on(
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
(id, message, timestamp) => {
_handleReceivedMessage(store, {
id,
message,
privateMessage: true,
timestamp,
nick: undefined
});
}
);
conference.on(
JitsiConferenceEvents.CONFERENCE_ERROR, (errorType, error) => {
errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);
});
}
/**
* Handles a chat error received from the xmpp server.
*
* @param {Store} store - The Redux store.
* @param {string} error - The error message.
* @returns {void}
*/
function _handleChatError({ dispatch }, error) {
dispatch(addMessage({
hasRead: true,
messageType: 'error',
message: error,
privateMessage: false,
timestamp: Date.now()
}));
}
/**
* Function to handle an incoming chat message.
*
* @param {Store} store - The Redux store.
* @param {Object} message - The message object.
* @returns {void}
*/
function _handleReceivedMessage({ dispatch, getState }, { id, message, nick, privateMessage, timestamp }) {
// Logic for all platforms:
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
if (!isChatOpen) {
dispatch(playSound(INCOMING_MSG_SOUND_ID));
}
// Provide a default for for the case when a message is being
// backfilled for a participant that has left the conference.
const participant = getParticipantById(state, id) || {};
const localParticipant = getLocalParticipant(getState);
const displayName = participant.name || nick || getParticipantDisplayName(state, id);
const hasRead = participant.local || isChatOpen;
const timestampToDate = timestamp
? new Date(timestamp) : new Date();
const millisecondsTimestamp = timestampToDate.getTime();
dispatch(addMessage({
displayName,
hasRead,
id,
messageType: participant.local ? 'local' : 'remote',
message,
privateMessage,
recipient: getParticipantDisplayName(state, localParticipant.id),
timestamp: millisecondsTimestamp
}));
if (typeof APP !== 'undefined') {
// Logic for web only:
APP.API.notifyReceivedChatMessage({
body: message,
id,
nick: displayName,
ts: timestamp
});
dispatch(showToolbox(4000));
}
}
/**
* Focuses the chat text field on web after the message recipient was updated, if needed.
*
* @returns {void}
*/
function _maybeFocusField() {
if (navigator.product !== 'ReactNative') {
const textField = document.getElementById('usermsg');
textField && textField.focus();
}
}
/**
* Persists the sent private messages as if they were received over the muc.
*
* This is required as we rely on the fact that we receive all messages from the muc that we send
* (as they are sent to everybody), but we don't receive the private messages we send to another participant.
* But those messages should be in the store as well, otherwise they don't appear in the chat window.
*
* @param {Store} store - The Redux store.
* @param {string} recipientID - The ID of the recipient the private message was sent to.
* @param {string} message - The sent message.
* @returns {void}
*/
function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message) {
const localParticipant = getLocalParticipant(getState);
const displayName = getParticipantDisplayName(getState, localParticipant.id);
dispatch(addMessage({
displayName,
hasRead: true,
id: localParticipant.id,
messageType: 'local',
message,
privateMessage: true,
recipient: getParticipantDisplayName(getState, recipientID),
timestamp: Date.now()
}));
}
/**
* Returns the ID of the participant who we may have wanted to send the message
* that we're about to send.
*
* @param {Object} state - The Redux state.
* @param {Object} action - The action being dispatched now.
* @returns {string?}
*/
function _shouldSendPrivateMessageTo(state, action): ?string {
if (action.ignorePrivacy) {
// Shortcut: this is only true, if we already displayed the notice, so no need to show it again.
return undefined;
}
const { messages, privateMessageRecipient } = state['features/chat'];
if (privateMessageRecipient) {
// We're already sending a private message, no need to warn about privacy.
return undefined;
}
if (!messages.length) {
// No messages yet, no need to warn for privacy.
return undefined;
}
// Platforms sort messages differently
const lastMessage = navigator.product === 'ReactNative'
? messages[0] : messages[messages.length - 1];
if (lastMessage.messageType === 'local') {
// The sender is probably aware of any private messages as already sent
// a message since then. Doesn't make sense to display the notice now.
return undefined;
}
if (lastMessage.privateMessage) {
// We show the notice if the last received message was private.
return lastMessage.id;
}
// But messages may come rapidly, we want to protect our users from mis-sending a message
// even when there was a reasonable recently received private message.
const now = Date.now();
const recentPrivateMessages = messages.filter(
message =>
message.messageType !== 'local'
&& message.privateMessage
&& message.timestamp + PRIVACY_NOTICE_TIMEOUT > now);
const recentPrivateMessage = navigator.product === 'ReactNative'
? recentPrivateMessages[0] : recentPrivateMessages[recentPrivateMessages.length - 1];
if (recentPrivateMessage) {
return recentPrivateMessage.id;
}
return undefined;
}

View File

@@ -2,12 +2,18 @@
import { ReducerRegistry } from '../base/redux';
import { ADD_MESSAGE, CLEAR_MESSAGES, TOGGLE_CHAT } from './actionTypes';
import {
ADD_MESSAGE,
CLEAR_MESSAGES,
SET_PRIVATE_MESSAGE_RECIPIENT,
TOGGLE_CHAT
} from './actionTypes';
const DEFAULT_STATE = {
isOpen: false,
lastReadMessage: undefined,
messages: []
messages: [],
privateMessageRecipient: undefined
};
ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
@@ -19,6 +25,8 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
id: action.id,
messageType: action.messageType,
message: action.message,
privateMessage: action.privateMessage,
recipient: action.recipient,
timestamp: action.timestamp
};
@@ -48,12 +56,20 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
messages: []
};
case SET_PRIVATE_MESSAGE_RECIPIENT:
return {
...state,
isOpen: Boolean(action.participant) || state.isOpen,
privateMessageRecipient: action.participant
};
case TOGGLE_CHAT:
return {
...state,
isOpen: !state.isOpen,
lastReadMessage: state.messages[
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1]
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
privateMessageRecipient: state.isOpen ? undefined : state.privateMessageRecipient
};
}

View File

@@ -16,6 +16,7 @@ import { TestConnectionInfo } from '../../../base/testing';
import { ConferenceNotification, isCalendarEnabled } from '../../../calendar-sync';
import { Chat } from '../../../chat';
import { DisplayNameLabel } from '../../../display-name';
import { SharedDocument } from '../../../etherpad';
import {
FILMSTRIP_SIZE,
Filmstrip,
@@ -161,107 +162,13 @@ class Conference extends AbstractConference<Props, *> {
* @returns {ReactElement}
*/
render() {
const {
_connecting,
_filmstripVisible,
_largeVideoParticipantId,
_reducedUI,
_shouldDisplayTileView,
_toolboxVisible
} = this.props;
const showGradient = _toolboxVisible;
const applyGradientStretching = _filmstripVisible && isNarrowAspectRatio(this) && !_shouldDisplayTileView;
return (
<Container style = { styles.conference }>
<StatusBar
barStyle = 'light-content'
hidden = { true }
translucent = { true } />
<Chat />
<AddPeopleDialog />
{/*
* The LargeVideo is the lowermost stacking layer.
*/
_shouldDisplayTileView
? <TileView onClick = { this._onClick } />
: <LargeVideo onClick = { this._onClick } />
}
{/*
* If there is a ringing call, show the callee's info.
*/
_reducedUI || <CalleeInfoContainer />
}
{/*
* The activity/loading indicator goes above everything, except
* the toolbox/toolbars and the dialogs.
*/
_connecting
&& <TintedView>
<LoadingIndicator />
</TintedView>
}
<View
pointerEvents = 'box-none'
style = { styles.toolboxAndFilmstripContainer }>
{ showGradient && <LinearGradient
colors = { NAVBAR_GRADIENT_COLORS }
end = {{
x: 0.0,
y: 0.0
}}
pointerEvents = 'none'
start = {{
x: 0.0,
y: 1.0
}}
style = { [
styles.bottomGradient,
applyGradientStretching ? styles.gradientStretchBottom : undefined
] } />}
<Labels />
<Captions onPress = { this._onClick } />
{ _shouldDisplayTileView || <DisplayNameLabel participantId = { _largeVideoParticipantId } /> }
{/*
* The Toolbox is in a stacking layer below the Filmstrip.
*/}
<Toolbox />
{/*
* The Filmstrip is in a stacking layer above the
* LargeVideo. The LargeVideo and the Filmstrip form what
* the Web/React app calls "videospace". Presumably, the
* name and grouping stem from the fact that these two
* React Components depict the videos of the conference's
* participants.
*/
_shouldDisplayTileView ? undefined : <Filmstrip />
}
</View>
<SafeAreaView
pointerEvents = 'box-none'
style = { styles.navBarSafeView }>
<NavigationBar />
{ this.renderNotificationsContainer() }
</SafeAreaView>
<TestConnectionInfo />
{
this._renderConferenceNotification()
}
{ this._renderContent() }
</Container>
);
}
@@ -320,6 +227,138 @@ class Conference extends AbstractConference<Props, *> {
: undefined);
}
/**
* Renders the content for the Conference container.
*
* @private
* @returns {React$Element}
*/
_renderContent() {
const {
_connecting,
_filmstripVisible,
_largeVideoParticipantId,
_reducedUI,
_shouldDisplayTileView,
_toolboxVisible
} = this.props;
const showGradient = _toolboxVisible;
const applyGradientStretching = _filmstripVisible && isNarrowAspectRatio(this) && !_shouldDisplayTileView;
if (_reducedUI) {
return this._renderContentForReducedUi();
}
return (
<>
<AddPeopleDialog />
<Chat />
<SharedDocument />
{/*
* The LargeVideo is the lowermost stacking layer.
*/
_shouldDisplayTileView
? <TileView onClick = { this._onClick } />
: <LargeVideo onClick = { this._onClick } />
}
{/*
* If there is a ringing call, show the callee's info.
*/
<CalleeInfoContainer />
}
{/*
* The activity/loading indicator goes above everything, except
* the toolbox/toolbars and the dialogs.
*/
_connecting
&& <TintedView>
<LoadingIndicator />
</TintedView>
}
<View
pointerEvents = 'box-none'
style = { styles.toolboxAndFilmstripContainer }>
{ showGradient && <LinearGradient
colors = { NAVBAR_GRADIENT_COLORS }
end = {{
x: 0.0,
y: 0.0
}}
pointerEvents = 'none'
start = {{
x: 0.0,
y: 1.0
}}
style = { [
styles.bottomGradient,
applyGradientStretching ? styles.gradientStretchBottom : undefined
] } />}
<Labels />
<Captions onPress = { this._onClick } />
{ _shouldDisplayTileView || <DisplayNameLabel participantId = { _largeVideoParticipantId } /> }
{/*
* The Toolbox is in a stacking layer below the Filmstrip.
*/}
<Toolbox />
{/*
* The Filmstrip is in a stacking layer above the
* LargeVideo. The LargeVideo and the Filmstrip form what
* the Web/React app calls "videospace". Presumably, the
* name and grouping stem from the fact that these two
* React Components depict the videos of the conference's
* participants.
*/
_shouldDisplayTileView ? undefined : <Filmstrip />
}
</View>
<SafeAreaView
pointerEvents = 'box-none'
style = { styles.navBarSafeView }>
<NavigationBar />
{ this._renderNotificationsContainer() }
</SafeAreaView>
<TestConnectionInfo />
{ this._renderConferenceNotification() }
</>
);
}
/**
* Renders the content for the Conference container when in "reduced UI" mode.
*
* @private
* @returns {React$Element}
*/
_renderContentForReducedUi() {
const { _connecting } = this.props;
return (
<>
<LargeVideo onClick = { this._onClick } />
{
_connecting
&& <TintedView>
<LoadingIndicator />
</TintedView>
}
</>
);
}
/**
* Renders a container for notifications to be displayed by the
* base/notifications feature.
@@ -327,7 +366,7 @@ class Conference extends AbstractConference<Props, *> {
* @private
* @returns {React$Element}
*/
renderNotificationsContainer() {
_renderNotificationsContainer() {
const notificationsStyle = {};
// In the landscape mode (wide) there's problem with notifications being

View File

@@ -5,7 +5,6 @@ import React from 'react';
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
import { obtainConfig } from '../../../base/config';
import { connect, disconnect } from '../../../base/connection';
import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux';
@@ -23,7 +22,6 @@ import {
} from '../../../toolbox';
import { maybeShowSuboptimalExperienceNotification } from '../../functions';
import logger from '../../logger';
import Labels from './Labels';
import { default as Notice } from './Notice';
@@ -123,31 +121,7 @@ class Conference extends AbstractConference<Props, *> {
*/
componentDidMount() {
document.title = interfaceConfig.APP_NAME;
const { configLocation } = config;
if (configLocation) {
obtainConfig(configLocation, this.props._room)
.then(() => {
const now = window.performance.now();
APP.connectionTimes['configuration.fetched'] = now;
logger.log('(TIME) configuration fetched:\t', now);
this._start();
})
.catch(err => {
logger.log(err);
// Show obtain config error.
APP.UI.messageHandler.showError({
descriptionKey: 'dialog.connectError',
titleKey: 'connection.CONNFAIL'
});
});
} else {
this._start();
}
this._start();
}
/**

View File

@@ -1,13 +1,3 @@
/**
* The type of the action which signals document editing has been enabled.
*
* {
* type: ETHERPAD_INITIALIZED
* }
*/
export const ETHERPAD_INITIALIZED = 'ETHERPAD_INITIALIZED';
/**
* The type of the action which signals document editing has stopped or started.
*
@@ -15,8 +5,16 @@ export const ETHERPAD_INITIALIZED = 'ETHERPAD_INITIALIZED';
* type: SET_DOCUMENT_EDITING_STATUS
* }
*/
export const SET_DOCUMENT_EDITING_STATUS
= 'SET_DOCUMENT_EDITING_STATUS';
export const SET_DOCUMENT_EDITING_STATUS = 'SET_DOCUMENT_EDITING_STATUS';
/**
* The type of the action which updates the shared document URL.
*
* {
* type: SET_DOCUMENT_URL
* }
*/
export const SET_DOCUMENT_URL = 'SET_DOCUMENT_URL';
/**
* The type of the action which signals to start or stop editing a shared

View File

@@ -1,8 +1,8 @@
// @flow
import {
ETHERPAD_INITIALIZED,
SET_DOCUMENT_EDITING_STATUS,
SET_DOCUMENT_URL,
TOGGLE_DOCUMENT_EDITING
} from './actionTypes';
@@ -24,15 +24,18 @@ export function setDocumentEditingState(editing: boolean) {
}
/**
* Dispatches an action to set Etherpad as having been initialized.
* Dispatches an action to set the shared document URL.
*
* @param {string} documentUrl - The shared document URL.
* @returns {{
* type: ETHERPAD_INITIALIZED
* type: SET_DOCUMENT_URL,
* documentUrl: string
* }}
*/
export function setEtherpadHasInitialzied() {
export function setDocumentUrl(documentUrl: ?string) {
return {
type: ETHERPAD_INITIALIZED
type: SET_DOCUMENT_URL,
documentUrl
};
}

View File

@@ -0,0 +1,81 @@
// @flow
import type { Dispatch } from 'redux';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { translate } from '../../base/i18n';
import { IconShareDoc } from '../../base/icons';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox';
import { toggleDocument } from '../actions';
type Props = AbstractButtonProps & {
/**
* Whether the shared document is being edited or not.
*/
_editing: boolean,
/**
* Redux dispatch function.
*/
dispatch: Dispatch<any>,
};
/**
* Implements an {@link AbstractButton} to open the chat screen on mobile.
*/
class SharedDocumentButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.document';
icon = IconShareDoc;
label = 'toolbar.documentOpen';
toggledLabel = 'toolbar.documentClose';
/**
* Handles clicking / pressing the button, and opens / closes the appropriate dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
sendAnalytics(createToolbarEvent(
'toggle.etherpad',
{
enable: !this.props._editing
}));
this.props.dispatch(toggleDocument());
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props._editing;
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {Object} state - The redux store/state.
* @param {Object} ownProps - The properties explicitly passed to the component
* instance.
* @returns {Object}
*/
function _mapStateToProps(state: Object, ownProps: Object) {
const { documentUrl, editing } = state['features/etherpad'];
const { visible = Boolean(documentUrl) } = ownProps;
return {
_editing: editing,
visible
};
}
export default translate(connect(_mapStateToProps)(SharedDocumentButton));

View File

@@ -0,0 +1,2 @@
export { default as SharedDocument } from './native/SharedDocument';
export { default as SharedDocumentButton } from './SharedDocumentButton';

View File

@@ -0,0 +1 @@
export { default as SharedDocumentButton } from './SharedDocumentButton';

View File

@@ -0,0 +1,178 @@
// @flow
import React, { PureComponent } from 'react';
import { SafeAreaView, View } from 'react-native';
import { WebView } from 'react-native-webview';
import type { Dispatch } from 'redux';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { HeaderWithNavigation, LoadingIndicator, SlidingView } from '../../../base/react';
import { connect } from '../../../base/redux';
import { toggleDocument } from '../../actions';
import { getSharedDocumentUrl } from '../../functions';
import styles, { INDICATOR_COLOR } from './styles';
/**
* The type of the React {@code Component} props of {@code ShareDocument}.
*/
type Props = {
/**
* URL for the shared document.
*/
_documentUrl: string,
/**
* Color schemed style of the header component.
*/
_headerStyles: Object,
/**
* True if the chat window should be rendered.
*/
_isOpen: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Dispatch<any>,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
/**
* Implements a React native component that renders the shared document window.
*/
class SharedDocument extends PureComponent<Props> {
/**
* Instantiates a new instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onClose = this._onClose.bind(this);
this._onError = this._onError.bind(this);
this._renderLoading = this._renderLoading.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const { _documentUrl, _isOpen } = this.props;
const webViewStyles = this._getWebViewStyles();
return (
<SlidingView
onHide = { this._onClose }
position = 'bottom'
show = { _isOpen } >
<View style = { styles.webViewWrapper }>
<HeaderWithNavigation
headerLabelKey = 'documentSharing.title'
onPressBack = { this._onClose } />
<SafeAreaView style = { webViewStyles }>
<WebView
onError = { this._onError }
renderLoading = { this._renderLoading }
source = {{ uri: _documentUrl }}
startInLoadingState = { true } />
</SafeAreaView>
</View>
</SlidingView>
);
}
/**
* Computes the styles required for the WebView component.
*
* @returns {Object}
*/
_getWebViewStyles() {
return {
...styles.webView,
backgroundColor: this.props._headerStyles.screenHeader.backgroundColor
};
}
_onClose: () => boolean
/**
* Closes the window.
*
* @returns {boolean}
*/
_onClose() {
const { _isOpen, dispatch } = this.props;
if (_isOpen) {
dispatch(toggleDocument());
return true;
}
return false;
}
_onError: () => void;
/**
* Callback to handle the error if the page fails to load.
*
* @returns {void}
*/
_onError() {
const { _isOpen, dispatch } = this.props;
if (_isOpen) {
dispatch(toggleDocument());
}
}
_renderLoading: () => React$Component<any>;
/**
* Renders the loading indicator.
*
* @returns {React$Component<any>}
*/
_renderLoading() {
return (
<View style = { styles.indicatorWrapper }>
<LoadingIndicator
color = { INDICATOR_COLOR }
size = 'large' />
</View>
);
}
}
/**
* Maps (parts of) the redux state to {@link SharedDocument} React {@code Component} props.
*
* @param {Object} state - The redux store/state.
* @private
* @returns {Object}
*/
export function _mapStateToProps(state: Object) {
const { editing } = state['features/etherpad'];
const documentUrl = getSharedDocumentUrl(state);
return {
_documentUrl: documentUrl,
_headerStyles: ColorSchemeRegistry.get(state, 'Header'),
_isOpen: editing
};
}
export default translate(connect(_mapStateToProps)(SharedDocument));

View File

@@ -0,0 +1,24 @@
// @flow
import { ColorPalette } from '../../../base/styles';
export const INDICATOR_COLOR = ColorPalette.lightGrey;
export default {
indicatorWrapper: {
alignItems: 'center',
backgroundColor: ColorPalette.white,
height: '100%',
justifyContent: 'center'
},
webView: {
flex: 1
},
webViewWrapper: {
flex: 1,
flexDirection: 'column'
}
};

View File

@@ -0,0 +1,34 @@
// @flow
import { toState } from '../base/redux';
const ETHERPAD_OPTIONS = {
showControls: 'true',
showChat: 'false',
showLineNumbers: 'true',
useMonospaceFont: 'false'
};
/**
* Retrieves the current sahred document URL.
*
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @returns {?string} - Current shared document URL or undefined.
*/
export function getSharedDocumentUrl(stateful: Function | Object) {
const state = toState(stateful);
const { documentUrl } = state['features/etherpad'];
const { displayName } = state['features/base/settings'];
if (!documentUrl) {
return undefined;
}
const params = new URLSearchParams(ETHERPAD_OPTIONS);
if (displayName) {
params.append('userName', displayName);
}
return `${documentUrl}?${params.toString()}`;
}

View File

@@ -1,5 +1,7 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './functions';
import './middleware';
import './reducer';

View File

@@ -1,12 +1,16 @@
// @flow
import { MiddlewareRegistry } from '../base/redux';
import { getCurrentConference } from '../base/conference';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import UIEvents from '../../../service/UI/UIEvents';
import { TOGGLE_DOCUMENT_EDITING } from './actionTypes';
import { setDocumentEditingState, setDocumentUrl } from './actions';
declare var APP: Object;
const ETHERPAD_COMMAND = 'etherpad';
/**
* Middleware that captures actions related to collaborative document editing
* and notifies components not hooked into redux.
@@ -15,16 +19,49 @@ declare var APP: Object;
* @returns {Function}
*/
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
if (typeof APP === 'undefined') {
return next(action);
}
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
switch (action.type) {
case TOGGLE_DOCUMENT_EDITING:
APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
case TOGGLE_DOCUMENT_EDITING: {
if (typeof APP === 'undefined') {
const { editing } = getState()['features/etherpad'];
dispatch(setDocumentEditingState(!editing));
} else {
APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
}
break;
}
}
return next(action);
});
/**
* Set up state change listener to perform maintenance tasks when the conference
* is left or failed, e.g. clear messages or close the chat modal if it's left
* open.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch, getState }, previousConference) => {
if (conference) {
conference.addCommandListener(ETHERPAD_COMMAND,
({ value }) => {
let url;
const { etherpad_base: etherpadBase } = getState()['features/base/config'];
if (etherpadBase) {
const u = new URL(value, etherpadBase);
url = u.toString();
}
dispatch(setDocumentUrl(url));
}
);
}
if (previousConference) {
dispatch(setDocumentUrl(undefined));
}
});

View File

@@ -2,28 +2,22 @@
import { ReducerRegistry } from '../base/redux';
import {
ETHERPAD_INITIALIZED,
SET_DOCUMENT_EDITING_STATUS
} from './actionTypes';
import { SET_DOCUMENT_EDITING_STATUS, SET_DOCUMENT_URL } from './actionTypes';
const DEFAULT_STATE = {
/**
* URL for the shared document.
*/
documentUrl: undefined,
/**
* Whether or not Etherpad is currently open.
*
* @public
* @type {boolean}
*/
editing: false,
/**
* Whether or not Etherpad is ready to use.
*
* @public
* @type {boolean}
*/
initialized: false
editing: false
};
/**
@@ -33,18 +27,18 @@ ReducerRegistry.register(
'features/etherpad',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case ETHERPAD_INITIALIZED:
return {
...state,
initialized: true
};
case SET_DOCUMENT_EDITING_STATUS:
return {
...state,
editing: action.editing
};
case SET_DOCUMENT_URL:
return {
...state,
documentUrl: action.documentUrl
};
default:
return state;
}

View File

@@ -5,7 +5,7 @@ import { NativeModules } from 'react-native';
let GoogleSignin;
if (NativeModules.RNGoogleSignin) {
GoogleSignin = require('react-native-google-signin').GoogleSignin;
GoogleSignin = require('@react-native-community/google-signin').GoogleSignin;
}
import {

View File

@@ -9,7 +9,7 @@ export default {
indicatorWrapper: {
alignItems: 'center',
backgroundColor: ColorPalette.white,
flex: 1,
height: '100%',
justifyContent: 'center'
},

View File

@@ -4,16 +4,26 @@ import React, { Component } from 'react';
import { Watermarks } from '../../base/react';
import { Captions } from '../../subtitles/';
import { connect } from '../../base/redux';
declare var interfaceConfig: Object;
type Props = {
/**
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
_noAutoPlayVideo: boolean
}
/**
* 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
*/
export default class LargeVideo extends Component<{}> {
class LargeVideo extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
@@ -51,7 +61,7 @@ export default class LargeVideo extends Component<{}> {
*/}
<div id = 'largeVideoWrapper'>
<video
autoPlay = { true }
autoPlay = { !this.props._noAutoPlayVideo }
id = 'largeVideo'
muted = { true } />
</div>
@@ -63,3 +73,24 @@ export default class LargeVideo extends Component<{}> {
);
}
}
/**
* Maps (parts of) the Redux state to the associated LargeVideo props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _noAutoPlayVideo: boolean
* }}
*/
function _mapStateToProps(state) {
const testingConfig = state['features/base/config'].testing;
return {
_noAutoPlayVideo: testingConfig?.noAutoPlayVideo
};
}
export default connect(_mapStateToProps)(LargeVideo);

View File

@@ -2,5 +2,4 @@
* The URL that is the main landing page for YouTube live streaming and should
* have a user's live stream key.
*/
export const YOUTUBE_LIVE_DASHBOARD_URL
= 'https://www.youtube.com/live_dashboard';
export const YOUTUBE_LIVE_DASHBOARD_URL = 'https://www.youtube.com/live_dashboard';

View File

@@ -1,12 +1,13 @@
// @flow
import React from 'react';
import { Linking, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { Text, TextInput, TouchableOpacity, View } from 'react-native';
import { _abstractMapStateToProps } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { StyleType } from '../../../../base/styles';
import { openURLInBrowser } from '../../../../base/util';
import AbstractStreamKeyForm, {
type Props as AbstractProps
@@ -120,7 +121,7 @@ class StreamKeyForm extends AbstractStreamKeyForm<Props> {
const { helpURL } = this;
if (typeof helpURL === 'string') {
Linking.openURL(helpURL);
openURLInBrowser(helpURL);
}
}
}

View File

@@ -2,7 +2,6 @@
import React, { Component } from 'react';
import {
Linking,
Text,
TouchableHighlight,
TouchableOpacity,
@@ -13,6 +12,7 @@ import { _abstractMapStateToProps } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { StyleType } from '../../../../base/styles';
import { openURLInBrowser } from '../../../../base/util';
import { YOUTUBE_LIVE_DASHBOARD_URL } from '../constants';
@@ -153,7 +153,7 @@ class StreamKeyPicker extends Component<Props, State> {
* @returns {void}
*/
_onOpenYoutubeDashboard() {
Linking.openURL(YOUTUBE_LIVE_DASHBOARD_URL);
openURLInBrowser(YOUTUBE_LIVE_DASHBOARD_URL);
}
_onStreamPick: string => Function

View File

@@ -9,6 +9,7 @@ import { BottomSheet, isDialogOpen } from '../../../base/dialog';
import { getParticipantDisplayName } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import { PrivateMessageButton } from '../../../chat';
import { hideRemoteVideoMenu } from '../../actions';
@@ -95,6 +96,7 @@ class RemoteVideoMenu extends Component<Props> {
<MuteButton { ...buttonProps } />
<KickButton { ...buttonProps } />
<PinButton { ...buttonProps } />
<PrivateMessageButton { ...buttonProps } />
</BottomSheet>
);
}

View File

@@ -0,0 +1,62 @@
// @flow
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
import { IconMessage } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { _mapDispatchToProps, _mapStateToProps, type Props } from '../../../chat/components/PrivateMessageButton';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
/**
* A custom implementation of the PrivateMessageButton specialized for
* the web version of the remote video menu. When the web platform starts to use
* the {@code AbstractButton} component for the remote video menu, we can get rid
* of this component and use the generic button in the chat feature.
*/
class PrivateMessageMenuButton extends Component<Props> {
/**
* Instantiates a new Component instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onClick = this._onClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
return (
<RemoteVideoMenuButton
buttonText = { t('toolbar.privateMessage') }
icon = { IconMessage }
id = { `privmsglink_${participantID}` }
onClick = { this._onClick } />
);
}
_onClick: () => void;
/**
* Callback to be invoked on pressing the button.
*
* @returns {void}
*/
_onClick() {
const { _participant, _setPrivateMessageRecipient } = this.props;
_setPrivateMessageRecipient(_participant);
}
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageMenuButton));

View File

@@ -8,6 +8,7 @@ import { Popover } from '../../../base/popover';
import {
MuteButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
RemoteVideoMenu,
VolumeSlider
@@ -188,6 +189,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
);
}
buttons.push(
<PrivateMessageMenuButton
key = 'privateMessage'
participantID = { participantID } />
);
if (onVolumeChange) {
buttons.push(
<VolumeSlider

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