Compare commits

...

13 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
44 changed files with 535 additions and 351 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

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

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

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

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

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

@@ -90,8 +90,7 @@
if ([RNGoogleSignin application:app
openURL:url
sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]
annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]) {
options:options]) {
return YES;
}

View File

@@ -573,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",
@@ -614,6 +615,7 @@
"exitTileView": "Exit tile view",
"feedback": "Leave feedback",
"hangup": "Leave",
"help": "Help",
"invite": "Invite people",
"login": "Login",
"logout": "Logout",

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

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

View File

@@ -8,8 +8,6 @@ import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
import parseURLParams from './parseURLParams';
import logger from './logger';
declare var $: Object;
// 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
@@ -40,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 */
/**

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

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

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

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

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

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

@@ -0,0 +1,56 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { translate } from '../../base/i18n';
import { IconHelp } from '../../base/icons';
import { connect } from '../../base/redux';
import { openURLInBrowser } from '../../base/util';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox';
type Props = AbstractButtonProps & {
/**
* The URL to the user documenation.
*/
_userDocumentationURL: string
};
/**
* Implements an {@link AbstractButton} to open the user documentation in a new window.
*/
class HelpButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.help';
icon = IconHelp;
label = 'toolbar.help';
/**
* Handles clicking / pressing the button, and opens a new window with the user documentation.
*
* @private
* @returns {void}
*/
_handleClick() {
sendAnalytics(createToolbarEvent('help.pressed'));
openURLInBrowser(this.props._userDocumentationURL);
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {Object} state - The redux store/state.
* @returns {Object}
*/
function _mapStateToProps(state: Object) {
const { userDocumentationURL } = state['features/base/config'];
const visible = typeof userDocumentationURL === 'string';
return {
_userDocumentationURL: userDocumentationURL,
visible
};
}
export default translate(connect(_mapStateToProps)(HelpButton));

View File

@@ -17,6 +17,7 @@ import { ClosedCaptionButton } from '../../../subtitles';
import { TileViewButton } from '../../../video-layout';
import AudioOnlyButton from './AudioOnlyButton';
import HelpButton from '../HelpButton';
import RaiseHandButton from './RaiseHandButton';
import ToggleCameraButton from './ToggleCameraButton';
@@ -110,6 +111,7 @@ class OverflowMenu extends Component<Props> {
}
<RaiseHandButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />
<HelpButton { ...buttonProps } />
</BottomSheet>
);
}

View File

@@ -71,6 +71,7 @@ import {
import AudioMuteButton from '../AudioMuteButton';
import { isToolboxVisible } from '../../functions';
import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton';
import OverflowMenuButton from './OverflowMenuButton';
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
import ToolbarButton from './ToolbarButton';
@@ -218,36 +219,22 @@ class Toolbox extends Component<Props, State> {
this._onSetOverflowVisible = this._onSetOverflowVisible.bind(this);
this._onShortcutToggleChat = this._onShortcutToggleChat.bind(this);
this._onShortcutToggleFullScreen
= this._onShortcutToggleFullScreen.bind(this);
this._onShortcutToggleRaiseHand
= this._onShortcutToggleRaiseHand.bind(this);
this._onShortcutToggleScreenshare
= this._onShortcutToggleScreenshare.bind(this);
this._onShortcutToggleVideoQuality
= this._onShortcutToggleVideoQuality.bind(this);
this._onToolbarOpenFeedback
= this._onToolbarOpenFeedback.bind(this);
this._onShortcutToggleFullScreen = this._onShortcutToggleFullScreen.bind(this);
this._onShortcutToggleRaiseHand = this._onShortcutToggleRaiseHand.bind(this);
this._onShortcutToggleScreenshare = this._onShortcutToggleScreenshare.bind(this);
this._onShortcutToggleVideoQuality = this._onShortcutToggleVideoQuality.bind(this);
this._onToolbarOpenFeedback = this._onToolbarOpenFeedback.bind(this);
this._onToolbarOpenInvite = this._onToolbarOpenInvite.bind(this);
this._onToolbarOpenKeyboardShortcuts
= this._onToolbarOpenKeyboardShortcuts.bind(this);
this._onToolbarOpenSpeakerStats
= this._onToolbarOpenSpeakerStats.bind(this);
this._onToolbarOpenVideoQuality
= this._onToolbarOpenVideoQuality.bind(this);
this._onToolbarOpenKeyboardShortcuts = this._onToolbarOpenKeyboardShortcuts.bind(this);
this._onToolbarOpenSpeakerStats = this._onToolbarOpenSpeakerStats.bind(this);
this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this);
this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
this._onToolbarToggleFullScreen
= this._onToolbarToggleFullScreen.bind(this);
this._onToolbarToggleProfile
= this._onToolbarToggleProfile.bind(this);
this._onToolbarToggleRaiseHand
= this._onToolbarToggleRaiseHand.bind(this);
this._onToolbarToggleScreenshare
= this._onToolbarToggleScreenshare.bind(this);
this._onToolbarToggleSharedVideo
= this._onToolbarToggleSharedVideo.bind(this);
this._onToolbarOpenLocalRecordingInfoDialog
= this._onToolbarOpenLocalRecordingInfoDialog.bind(this);
this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this);
this._onToolbarToggleProfile = this._onToolbarToggleProfile.bind(this);
this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
this._onToolbarToggleSharedVideo = this._onToolbarToggleSharedVideo.bind(this);
this._onToolbarOpenLocalRecordingInfoDialog = this._onToolbarOpenLocalRecordingInfoDialog.bind(this);
this.state = {
windowWidth: window.innerWidth
@@ -936,16 +923,11 @@ class Toolbox extends Component<Props, State> {
onClick = { this._onToolbarOpenVideoQuality } />,
this._shouldShowButton('fullscreen')
&& <OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.fullScreen') }
icon = { _fullScreen
? IconExitFullScreen
: IconFullScreen }
accessibilityLabel = { t('toolbar.accessibilityLabel.fullScreen') }
icon = { _fullScreen ? IconExitFullScreen : IconFullScreen }
key = 'fullscreen'
onClick = { this._onToolbarToggleFullScreen }
text = { _fullScreen
? t('toolbar.exitFullScreen')
: t('toolbar.enterFullScreen') } />,
text = { _fullScreen ? t('toolbar.exitFullScreen') : t('toolbar.enterFullScreen') } />,
<LiveStreamButton
key = 'livestreaming'
showLabel = { true } />,
@@ -954,14 +936,11 @@ class Toolbox extends Component<Props, State> {
showLabel = { true } />,
this._shouldShowButton('sharedvideo')
&& <OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.sharedvideo') }
accessibilityLabel = { t('toolbar.accessibilityLabel.sharedvideo') }
icon = { IconShareVideo }
key = 'sharedvideo'
onClick = { this._onToolbarToggleSharedVideo }
text = { _sharingVideo
? t('toolbar.stopSharedVideo')
: t('toolbar.sharedvideo') } />,
text = { _sharingVideo ? t('toolbar.stopSharedVideo') : t('toolbar.sharedvideo') } />,
this._shouldShowButton('etherpad')
&& <SharedDocumentButton
key = 'etherpad'
@@ -976,8 +955,7 @@ class Toolbox extends Component<Props, State> {
visible = { this._shouldShowButton('settings') } />,
this._shouldShowButton('stats')
&& <OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.speakerStats') }
accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }
icon = { IconPresentation }
key = 'stats'
onClick = { this._onToolbarOpenSpeakerStats }
@@ -985,20 +963,22 @@ class Toolbox extends Component<Props, State> {
this._shouldShowButton('feedback')
&& _feedbackConfigured
&& <OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.feedback') }
accessibilityLabel = { t('toolbar.accessibilityLabel.feedback') }
icon = { IconFeedback }
key = 'feedback'
onClick = { this._onToolbarOpenFeedback }
text = { t('toolbar.feedback') } />,
this._shouldShowButton('shortcuts')
&& <OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.shortcuts') }
accessibilityLabel = { t('toolbar.accessibilityLabel.shortcuts') }
icon = { IconOpenInNew }
key = 'shortcuts'
onClick = { this._onToolbarOpenKeyboardShortcuts }
text = { t('toolbar.shortcuts') } />
text = { t('toolbar.shortcuts') } />,
this._shouldShowButton('help')
&& <HelpButton
key = 'help'
showLabel = { true } />
];
}
@@ -1057,8 +1037,7 @@ class Toolbox extends Component<Props, State> {
case 'invite':
return (
<OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.invite') }
accessibilityLabel = { t('toolbar.accessibilityLabel.invite') }
icon = { IconInvite }
key = 'invite'
onClick = { this._onToolbarOpenInvite }
@@ -1069,13 +1048,10 @@ class Toolbox extends Component<Props, State> {
case 'localrecording':
return (
<OverflowMenuItem
accessibilityLabel
= { t('toolbar.accessibilityLabel.localRecording') }
accessibilityLabel = { t('toolbar.accessibilityLabel.localRecording') }
icon = { IconRec }
key = 'localrecording'
onClick = {
this._onToolbarOpenLocalRecordingInfoDialog
}
onClick = { this._onToolbarOpenLocalRecordingInfoDialog }
text = { t('localRecording.dialogTitle') } />
);
default:
@@ -1098,8 +1074,7 @@ class Toolbox extends Component<Props, State> {
t
} = this.props;
const overflowMenuContent = this._renderOverflowMenuContent();
const overflowHasItems = Boolean(overflowMenuContent.filter(
child => child).length);
const overflowHasItems = Boolean(overflowMenuContent.filter(child => child).length);
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const buttonsLeft = [];
const buttonsRight = [];
@@ -1182,10 +1157,7 @@ class Toolbox extends Component<Props, State> {
&& this._renderDesktopSharingButton() }
{ buttonsLeft.indexOf('raisehand') !== -1
&& <ToolbarButton
accessibilityLabel =
{
t('toolbar.accessibilityLabel.raiseHand')
}
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
icon = { IconRaisedHand }
onClick = { this._onToolbarToggleRaiseHand }
toggled = { _raisedHand }
@@ -1193,8 +1165,7 @@ class Toolbox extends Component<Props, State> {
{ buttonsLeft.indexOf('chat') !== -1
&& <div className = 'toolbar-button-with-badge'>
<ToolbarButton
accessibilityLabel =
{ t('toolbar.accessibilityLabel.chat') }
accessibilityLabel = { t('toolbar.accessibilityLabel.chat') }
icon = { IconChat }
onClick = { this._onToolbarToggleChat }
toggled = { _chatOpen }

View File

@@ -53,6 +53,15 @@ class WelcomePage extends AbstractWelcomePage {
*/
this._additionalContentRef = null;
/**
* The HTML Element used as the container for additional toolbar content. Used
* for directly appending the additional content template to the dom.
*
* @private
* @type {HTMLTemplateElement|null}
*/
this._additionalToolbarContentRef = null;
/**
* The template to use as the main content for the welcome page. If
* not found then only the welcome page head will display.
@@ -63,11 +72,24 @@ class WelcomePage extends AbstractWelcomePage {
this._additionalContentTemplate = document.getElementById(
'welcome-page-additional-content-template');
/**
* The template to use as the additional content for the welcome page header toolbar.
* If not found then only the settings icon will be displayed.
*
* @private
* @type {HTMLTemplateElement|null}
*/
this._additionalToolbarContentTemplate = document.getElementById(
'settings-toolbar-additional-content-template'
);
// Bind event handlers so they are only bound once per instance.
this._onFormSubmit = this._onFormSubmit.bind(this);
this._onRoomChange = this._onRoomChange.bind(this);
this._setAdditionalContentRef
= this._setAdditionalContentRef.bind(this);
this._setAdditionalToolbarContentRef
= this._setAdditionalToolbarContentRef.bind(this);
this._onTabSelected = this._onTabSelected.bind(this);
}
@@ -90,6 +112,12 @@ class WelcomePage extends AbstractWelcomePage {
this._additionalContentRef.appendChild(
this._additionalContentTemplate.content.cloneNode(true));
}
if (this._shouldShowAdditionalToolbarContent()) {
this._additionalToolbarContentRef.appendChild(
this._additionalToolbarContentTemplate.content.cloneNode(true)
);
}
}
/**
@@ -114,6 +142,7 @@ class WelcomePage extends AbstractWelcomePage {
const { t } = this.props;
const { APP_NAME } = interfaceConfig;
const showAdditionalContent = this._shouldShowAdditionalContent();
const showAdditionalToolbarContent = this._shouldShowAdditionalToolbarContent();
return (
<div
@@ -127,6 +156,12 @@ class WelcomePage extends AbstractWelcomePage {
<div className = 'welcome-page-settings'>
<SettingsButton
defaultTab = { SETTINGS_TABS.CALENDAR } />
{ showAdditionalToolbarContent
? <div
className = 'settings-toolbar-content'
ref = { this._setAdditionalToolbarContentRef } />
: null
}
</div>
<div className = 'header-image' />
<div className = 'header-text'>
@@ -263,6 +298,19 @@ class WelcomePage extends AbstractWelcomePage {
this._additionalContentRef = el;
}
/**
* Sets the internal reference to the HTMLDivElement used to hold the
* toolbar additional content.
*
* @param {HTMLDivElement} el - The HTMLElement for the div that is the root
* of the additional toolbar content.
* @private
* @returns {void}
*/
_setAdditionalToolbarContentRef(el) {
this._additionalToolbarContentRef = el;
}
/**
* Returns whether or not additional content should be displayed below
* the welcome page's header for entering a room name.
@@ -276,6 +324,20 @@ class WelcomePage extends AbstractWelcomePage {
&& this._additionalContentTemplate.content
&& this._additionalContentTemplate.innerHTML.trim();
}
/**
* Returns whether or not additional content should be displayed inside
* the header toolbar.
*
* @private
* @returns {boolean}
*/
_shouldShowAdditionalToolbarContent() {
return interfaceConfig.DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT
&& this._additionalToolbarContentTemplate
&& this._additionalToolbarContentTemplate.content
&& this._additionalToolbarContentTemplate.innerHTML.trim();
}
}
export default translate(connect(_mapStateToProps)(WelcomePage));

View File

@@ -0,0 +1 @@
<template id="settings-toolbar-additional-content-template"></template>