Compare commits
42 Commits
perf-test
...
android-sd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dede23f5b4 | ||
|
|
c669075265 | ||
|
|
55983ff62a | ||
|
|
b4be1bcd05 | ||
|
|
2420a68be9 | ||
|
|
ebfc5a95ff | ||
|
|
68cad276bd | ||
|
|
e683d70a18 | ||
|
|
9645391180 | ||
|
|
851b1a76a9 | ||
|
|
4890390ea2 | ||
|
|
7828bf8d46 | ||
|
|
191da551e3 | ||
|
|
55f35933e8 | ||
|
|
b125bff7c7 | ||
|
|
c1d261445e | ||
|
|
c494d6c48b | ||
|
|
4134d47f6e | ||
|
|
0b25e62c5c | ||
|
|
4d0cbff5a1 | ||
|
|
c79463aaee | ||
|
|
339e1c5fab | ||
|
|
36455c24c8 | ||
|
|
a622a4c713 | ||
|
|
1aaaae24ee | ||
|
|
9191000da4 | ||
|
|
8eb93086bd | ||
|
|
b64294af6d | ||
|
|
bbf33a8895 | ||
|
|
bcc1289a23 | ||
|
|
58bd48c1ae | ||
|
|
1a3736bf98 | ||
|
|
0eec182df4 | ||
|
|
c526844eb2 | ||
|
|
d856c1f328 | ||
|
|
15e47a9eb3 | ||
|
|
da98d39b61 | ||
|
|
411bafb5a6 | ||
|
|
0a64bf2068 | ||
|
|
db6a2673de | ||
|
|
e11d4d3101 | ||
|
|
8fd3bb2302 |
11
README.md
@@ -29,11 +29,18 @@ You can download source archives (produced by ```make source-package```):
|
||||
|
||||
### Mobile apps
|
||||
|
||||
You can get our mobile versions from here:
|
||||
|
||||
* [Android](https://play.google.com/store/apps/details?id=org.jitsi.meet)
|
||||
|
||||
[<img src="resources/img/google-play-badge.png" height="50">](https://play.google.com/store/apps/details?id=org.jitsi.meet)
|
||||
|
||||
* [Android (F-Droid)](https://f-droid.org/en/packages/org.jitsi.meet/)
|
||||
|
||||
[<img src="resources/img/f-droid-badge.png" height="50">](https://f-droid.org/en/packages/org.jitsi.meet/)
|
||||
|
||||
* [iOS](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
|
||||
|
||||
[<img src="resources/img/appstore-badge.png" height="50">](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
|
||||
|
||||
You can also sign up for our open beta testing here:
|
||||
|
||||
* [Android](https://play.google.com/apps/testing/org.jitsi.meet)
|
||||
|
||||
@@ -69,7 +69,9 @@ repositories {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-5'
|
||||
|
||||
if (!rootProject.ext.libreBuild) {
|
||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||
@@ -83,9 +85,6 @@ dependencies {
|
||||
}
|
||||
|
||||
implementation project(':sdk')
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
|
||||
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
|
||||
}
|
||||
|
||||
gradle.projectsEvaluated {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:name=".MainApplication"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* 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;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
|
||||
/**
|
||||
* Simple {@link Application} for hooking up LeakCanary:
|
||||
* https://github.com/square/leakcanary
|
||||
*/
|
||||
public class MainApplication extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
if (!LeakCanary.isInAnalyzerProcess(this)) {
|
||||
LeakCanary.install(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'io.fabric.tools:gradle:1.27.0'
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
classpath 'io.fabric.tools:gradle:1.28.1'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files.
|
||||
@@ -165,50 +165,6 @@ ext {
|
||||
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
|
||||
}
|
||||
|
||||
// If Android SDK is not installed, accept its license so that it
|
||||
// is automatically downloaded.
|
||||
afterEvaluate { project ->
|
||||
// Either the environment variable ANDROID_HOME or the property sdk.dir in
|
||||
// local.properties identifies where Android SDK is installed.
|
||||
def androidHome = System.env.ANDROID_HOME
|
||||
if (!androidHome) {
|
||||
// ANDROID_HOME is not set. Is sdk.dir set?
|
||||
def file = file("${project.rootDir}/local.properties")
|
||||
def props = new Properties()
|
||||
if (file.canRead()) {
|
||||
file.withInputStream {
|
||||
props.load(it)
|
||||
androidHome = props.'sdk.dir'
|
||||
}
|
||||
}
|
||||
if (!androidHome && (!file.exists() || file.canWrite())) {
|
||||
// Neither ANDROID_HOME nor sdk.dir is set. Set sdk.dir (because
|
||||
// environment variables cannot be set).
|
||||
props.'sdk.dir' = "${project.buildDir}/android-sdk".toString()
|
||||
file.withOutputStream {
|
||||
props.store(it, null)
|
||||
androidHome = props.'sdk.dir'
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the license is not accepted, accept it so that automatic downloading
|
||||
// kicks in.
|
||||
// The license hash can be taken from the accepted licenses, by doing this
|
||||
// on your local machine the file is
|
||||
// ${androidHome}/licenses/android-sdk-license
|
||||
if (androidHome) {
|
||||
def dir = file("${androidHome}/licenses")
|
||||
dir.mkdirs()
|
||||
def file = file("${dir.path}/android-sdk-license")
|
||||
if (!file.exists()) {
|
||||
file.withWriter {
|
||||
def hash = 'd56f5187479451eabf01fb78af6dfcb131a6481e'
|
||||
it.write(hash, 0, hash.length())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force the version of the Android build tools we have chosen on all
|
||||
// subprojects. The forcing was introduced for react-native and the third-party
|
||||
// modules that we utilize such as react-native-background-timer.
|
||||
|
||||
@@ -37,10 +37,12 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.fragment:fragment:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
//noinspection GradleDynamicVersion
|
||||
api 'com.facebook.react:react-native:+'
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation 'org.webkit:android-jsc:+'
|
||||
|
||||
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'
|
||||
|
||||
@@ -53,7 +53,7 @@ class AudioDeviceHandlerConnectionService implements
|
||||
*/
|
||||
private static int audioDeviceToRouteInt(String audioDevice) {
|
||||
if (audioDevice == null) {
|
||||
return CallAudioState.ROUTE_EARPIECE;
|
||||
return CallAudioState.ROUTE_SPEAKER;
|
||||
}
|
||||
switch (audioDevice) {
|
||||
case AudioModeModule.DEVICE_BLUETOOTH:
|
||||
@@ -66,7 +66,7 @@ class AudioDeviceHandlerConnectionService implements
|
||||
return CallAudioState.ROUTE_SPEAKER;
|
||||
default:
|
||||
JitsiMeetLogger.e(TAG + " Unsupported device name: " + audioDevice);
|
||||
return CallAudioState.ROUTE_EARPIECE;
|
||||
return CallAudioState.ROUTE_SPEAKER;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,14 +123,17 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
* {@link android.telecom.Connection#STATE_ACTIVE}.
|
||||
*
|
||||
* @param callUUID the call UUID which identifies the connection.
|
||||
* @return Whether the connection was set as active or not.
|
||||
*/
|
||||
static void setConnectionActive(String callUUID) {
|
||||
static boolean setConnectionActive(String callUUID) {
|
||||
ConnectionImpl connection = connections.get(callUUID);
|
||||
|
||||
if (connection != null) {
|
||||
connection.setActive();
|
||||
return true;
|
||||
} else {
|
||||
JitsiMeetLogger.e("%s setConnectionActive - no connection for UUID: %s", TAG, callUUID);
|
||||
JitsiMeetLogger.w("%s setConnectionActive - no connection for UUID: %s", TAG, callUUID);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,15 +105,22 @@ class RNConnectionService extends ReactContextBaseJavaModule {
|
||||
|
||||
ConnectionService.registerStartCallPromise(callUUID, promise);
|
||||
|
||||
try {
|
||||
TelecomManager tm
|
||||
= (TelecomManager) ctx.getSystemService(
|
||||
Context.TELECOM_SERVICE);
|
||||
TelecomManager tm = null;
|
||||
|
||||
try {
|
||||
tm = (TelecomManager) ctx.getSystemService(Context.TELECOM_SERVICE);
|
||||
tm.placeCall(address, extras);
|
||||
} catch (Exception e) {
|
||||
JitsiMeetLogger.e(e, TAG + " error in startCall");
|
||||
if (tm != null) {
|
||||
tm.unregisterPhoneAccount(accountHandle);
|
||||
}
|
||||
ConnectionService.unregisterStartCallPromise(callUUID);
|
||||
promise.reject(e);
|
||||
if (e instanceof SecurityException) {
|
||||
promise.reject("SECURITY_ERROR", "Required permissions not granted.");
|
||||
} else {
|
||||
promise.reject(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +158,11 @@ class RNConnectionService extends ReactContextBaseJavaModule {
|
||||
@ReactMethod
|
||||
public void reportConnectedOutgoingCall(String callUUID, Promise promise) {
|
||||
JitsiMeetLogger.d(TAG + " reportConnectedOutgoingCall " + callUUID);
|
||||
ConnectionService.setConnectionActive(callUUID);
|
||||
promise.resolve(null);
|
||||
if (ConnectionService.setConnectionActive(callUUID)) {
|
||||
promise.resolve(null);
|
||||
} else {
|
||||
promise.reject("CONNECTION_NOT_FOUND_ERROR", "Connection wasn't found.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
261
conference.js
@@ -93,10 +93,15 @@ import {
|
||||
participantRoleChanged,
|
||||
participantUpdated
|
||||
} from './react/features/base/participants';
|
||||
import { updateSettings } from './react/features/base/settings';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
updateSettings
|
||||
} from './react/features/base/settings';
|
||||
import {
|
||||
createLocalPresenterTrack,
|
||||
createLocalTracksF,
|
||||
destroyLocalTracks,
|
||||
isLocalVideoTrackMuted,
|
||||
isLocalTrackMuted,
|
||||
isUserInteractionRequiredForUnmute,
|
||||
replaceLocalTrack,
|
||||
@@ -113,7 +118,9 @@ import {
|
||||
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
|
||||
import { suspendDetected } from './react/features/power-monitor';
|
||||
import { setSharedVideoStatus } from './react/features/shared-video';
|
||||
import { createPresenterEffect } from './react/features/stream-effects/presenter';
|
||||
import { endpointMessageReceived } from './react/features/subtitles';
|
||||
import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -437,6 +444,11 @@ export default {
|
||||
*/
|
||||
localAudio: null,
|
||||
|
||||
/**
|
||||
* The local presenter video track (if any).
|
||||
*/
|
||||
localPresenterVideo: null,
|
||||
|
||||
/**
|
||||
* The local video track (if any).
|
||||
* FIXME tracks from redux store should be the single source of truth, but
|
||||
@@ -468,14 +480,18 @@ export default {
|
||||
audioOnlyError,
|
||||
screenSharingError,
|
||||
videoOnlyError;
|
||||
const initialDevices = [];
|
||||
let requestedAudio = false;
|
||||
const initialDevices = [ 'audio' ];
|
||||
const requestedAudio = true;
|
||||
let requestedVideo = false;
|
||||
|
||||
if (!options.startWithAudioMuted) {
|
||||
initialDevices.push('audio');
|
||||
requestedAudio = true;
|
||||
// Always get a handle on the audio input device so that we have statistics even if the user joins the
|
||||
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
|
||||
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
|
||||
// only after that point.
|
||||
if (options.startWithAudioMuted) {
|
||||
this.muteAudio(true, true);
|
||||
}
|
||||
|
||||
if (!options.startWithVideoMuted
|
||||
&& !options.startAudioOnly
|
||||
&& !options.startScreenSharing) {
|
||||
@@ -722,9 +738,8 @@ export default {
|
||||
isLocalVideoMuted() {
|
||||
// If the tracks are not ready, read from base/media state
|
||||
return this._localTracksInitialized
|
||||
? isLocalTrackMuted(
|
||||
APP.store.getState()['features/base/tracks'],
|
||||
MEDIA_TYPE.VIDEO)
|
||||
? isLocalVideoTrackMuted(
|
||||
APP.store.getState()['features/base/tracks'])
|
||||
: isVideoMutedByUser(APP.store);
|
||||
},
|
||||
|
||||
@@ -798,6 +813,35 @@ export default {
|
||||
this.muteAudio(!this.isLocalAudioMuted(), showUI);
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates toolbar button click for presenter video mute. Used by
|
||||
* shortcuts and API.
|
||||
* @param mute true for mute and false for unmute.
|
||||
* @param {boolean} [showUI] when set to false will not display any error
|
||||
* dialogs in case of media permissions error.
|
||||
*/
|
||||
async mutePresenter(mute, showUI = true) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
showUI && APP.store.dispatch(notifyCameraError(error));
|
||||
};
|
||||
|
||||
if (mute) {
|
||||
try {
|
||||
await this.localVideo.setEffect(undefined);
|
||||
} catch (err) {
|
||||
logger.error('Failed to remove the presenter effect', err);
|
||||
maybeShowErrorDialog(err);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await this.localVideo.setEffect(await this._createPresenterStreamEffect());
|
||||
} catch (err) {
|
||||
logger.error('Failed to apply the presenter effect', err);
|
||||
maybeShowErrorDialog(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates toolbar button click for video mute. Used by shortcuts and API.
|
||||
* @param mute true for mute and false for unmute.
|
||||
@@ -812,6 +856,10 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSharingScreen) {
|
||||
return this._mutePresenterVideo(mute);
|
||||
}
|
||||
|
||||
// If not ready to modify track's state yet adjust the base/media
|
||||
if (!this._localTracksInitialized) {
|
||||
// This will only modify base/media.video.muted which is then synced
|
||||
@@ -1220,6 +1268,7 @@ export default {
|
||||
options.applicationName = interfaceConfig.APP_NAME;
|
||||
options.getWiFiStatsMethod = this._getWiFiStatsMethod;
|
||||
options.confID = `${locationURL.host}${locationURL.pathname}`;
|
||||
options.createVADProcessor = createRnnoiseProcessorPromise;
|
||||
|
||||
return options;
|
||||
},
|
||||
@@ -1351,7 +1400,7 @@ export default {
|
||||
* in case it fails.
|
||||
* @private
|
||||
*/
|
||||
_turnScreenSharingOff(didHaveVideo, wasVideoMuted) {
|
||||
_turnScreenSharingOff(didHaveVideo) {
|
||||
this._untoggleScreenSharing = null;
|
||||
this.videoSwitchInProgress = true;
|
||||
const { receiver } = APP.remoteControl;
|
||||
@@ -1369,13 +1418,7 @@ export default {
|
||||
.then(([ stream ]) => this.useVideoStream(stream))
|
||||
.then(() => {
|
||||
sendAnalytics(createScreenSharingEvent('stopped'));
|
||||
logger.log('Screen sharing stopped, switching to video.');
|
||||
|
||||
if (!this.localVideo && wasVideoMuted) {
|
||||
return Promise.reject('No local video to be muted!');
|
||||
} else if (wasVideoMuted && this.localVideo) {
|
||||
return this.localVideo.mute();
|
||||
}
|
||||
logger.log('Screen sharing stopped.');
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('failed to switch back to local video', error);
|
||||
@@ -1390,6 +1433,16 @@ export default {
|
||||
promise = this.useVideoStream(null);
|
||||
}
|
||||
|
||||
// mute the presenter track if it exists.
|
||||
if (this.localPresenterVideo) {
|
||||
APP.store.dispatch(
|
||||
setVideoMuted(true, MEDIA_TYPE.PRESENTER));
|
||||
this.localPresenterVideo.dispose();
|
||||
APP.store.dispatch(
|
||||
trackRemoved(this.localPresenterVideo));
|
||||
this.localPresenterVideo = null;
|
||||
}
|
||||
|
||||
return promise.then(
|
||||
() => {
|
||||
this.videoSwitchInProgress = false;
|
||||
@@ -1415,7 +1468,7 @@ export default {
|
||||
* 'window', etc.).
|
||||
* @return {Promise.<T>}
|
||||
*/
|
||||
toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
|
||||
async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
|
||||
if (this.videoSwitchInProgress) {
|
||||
return Promise.reject('Switch in progress.');
|
||||
}
|
||||
@@ -1429,7 +1482,15 @@ export default {
|
||||
}
|
||||
|
||||
if (toggle) {
|
||||
return this._switchToScreenSharing(options);
|
||||
try {
|
||||
await this._switchToScreenSharing(options);
|
||||
|
||||
return;
|
||||
} catch (err) {
|
||||
logger.error('Failed to switch to screensharing', err);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return this._untoggleScreenSharing
|
||||
@@ -1454,8 +1515,7 @@ export default {
|
||||
_createDesktopTrack(options = {}) {
|
||||
let externalInstallation = false;
|
||||
let DSExternalInstallationInProgress = false;
|
||||
const didHaveVideo = Boolean(this.localVideo);
|
||||
const wasVideoMuted = this.isLocalVideoMuted();
|
||||
const didHaveVideo = !this.isLocalVideoMuted();
|
||||
|
||||
const getDesktopStreamPromise = options.desktopStream
|
||||
? Promise.resolve([ options.desktopStream ])
|
||||
@@ -1506,8 +1566,7 @@ export default {
|
||||
// Stores the "untoggle" handler which remembers whether was
|
||||
// there any video before and whether was it muted.
|
||||
this._untoggleScreenSharing
|
||||
= this._turnScreenSharingOff
|
||||
.bind(this, didHaveVideo, wasVideoMuted);
|
||||
= this._turnScreenSharingOff.bind(this, didHaveVideo);
|
||||
desktopStream.on(
|
||||
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => {
|
||||
@@ -1532,6 +1591,77 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new instance of presenter effect. A new video track is created
|
||||
* using the new set of constraints that are calculated based on
|
||||
* the height of the desktop that is being currently shared.
|
||||
*
|
||||
* @param {number} height - The height of the desktop stream that is being
|
||||
* currently shared.
|
||||
* @param {string} cameraDeviceId - The device id of the camera to be used.
|
||||
* @return {Promise<JitsiStreamPresenterEffect>} - A promise resolved with
|
||||
* {@link JitsiStreamPresenterEffect} if it succeeds.
|
||||
*/
|
||||
async _createPresenterStreamEffect(height = null, cameraDeviceId = null) {
|
||||
if (!this.localPresenterVideo) {
|
||||
try {
|
||||
this.localPresenterVideo = await createLocalPresenterTrack({ cameraDeviceId }, height);
|
||||
} catch (err) {
|
||||
logger.error('Failed to create a camera track for presenter', err);
|
||||
|
||||
return;
|
||||
}
|
||||
APP.store.dispatch(trackAdded(this.localPresenterVideo));
|
||||
}
|
||||
try {
|
||||
const effect = await createPresenterEffect(this.localPresenterVideo.stream);
|
||||
|
||||
return effect;
|
||||
} catch (err) {
|
||||
logger.error('Failed to create the presenter effect', err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Tries to turn the presenter video track on or off. If a presenter track
|
||||
* doesn't exist, a new video track is created.
|
||||
*
|
||||
* @param mute - true for mute and false for unmute.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async _mutePresenterVideo(mute) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
APP.store.dispatch(notifyCameraError(error));
|
||||
};
|
||||
|
||||
if (!this.localPresenterVideo && !mute) {
|
||||
// create a new presenter track and apply the presenter effect.
|
||||
const { height } = this.localVideo.track.getSettings();
|
||||
const defaultCamera
|
||||
= getUserSelectedCameraDeviceId(APP.store.getState());
|
||||
let effect;
|
||||
|
||||
try {
|
||||
effect = await this._createPresenterStreamEffect(height,
|
||||
defaultCamera);
|
||||
} catch (err) {
|
||||
logger.error('Failed to unmute Presenter Video');
|
||||
maybeShowErrorDialog(err);
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.localVideo.setEffect(effect);
|
||||
APP.store.dispatch(setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
|
||||
} catch (err) {
|
||||
logger.error('Failed to apply the Presenter effect', err);
|
||||
}
|
||||
} else {
|
||||
APP.store.dispatch(setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Tries to switch to the screensharing mode by disposing camera stream and
|
||||
* replacing it with a desktop one.
|
||||
@@ -1992,36 +2122,62 @@ export default {
|
||||
const videoWasMuted = this.isLocalVideoMuted();
|
||||
|
||||
sendAnalytics(createDeviceChangedEvent('video', 'input'));
|
||||
createLocalTracksF({
|
||||
devices: [ 'video' ],
|
||||
cameraDeviceId,
|
||||
micDeviceId: null
|
||||
})
|
||||
.then(([ stream ]) => {
|
||||
// if we are in audio only mode or video was muted before
|
||||
// changing device, then mute
|
||||
if (this.isAudioOnly() || videoWasMuted) {
|
||||
return stream.mute()
|
||||
.then(() => stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
})
|
||||
.then(stream => {
|
||||
// if we are screen sharing we do not want to stop it
|
||||
if (this.isSharingScreen) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// If both screenshare and video are in progress, restart the
|
||||
// presenter mode with the new camera device.
|
||||
if (this.isSharingScreen && !videoWasMuted) {
|
||||
const { height } = this.localVideo.track.getSettings();
|
||||
|
||||
return this.useVideoStream(stream);
|
||||
})
|
||||
.then(() => {
|
||||
// dispose the existing presenter track and create a new
|
||||
// camera track.
|
||||
this.localPresenterVideo.dispose();
|
||||
this.localPresenterVideo = null;
|
||||
|
||||
return this._createPresenterStreamEffect(height, cameraDeviceId)
|
||||
.then(effect => this.localVideo.setEffect(effect))
|
||||
.then(() => {
|
||||
this.setVideoMuteStatus(false);
|
||||
logger.log('switched local video device');
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => APP.store.dispatch(notifyCameraError(err)));
|
||||
|
||||
// If screenshare is in progress but video is muted, update the default device
|
||||
// id for video, dispose the existing presenter track and create a new effect
|
||||
// that can be applied on un-mute.
|
||||
} else if (this.isSharingScreen && videoWasMuted) {
|
||||
logger.log('switched local video device');
|
||||
const { height } = this.localVideo.track.getSettings();
|
||||
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => {
|
||||
APP.store.dispatch(notifyCameraError(err));
|
||||
});
|
||||
this.localPresenterVideo.dispose();
|
||||
this.localPresenterVideo = null;
|
||||
this._createPresenterStreamEffect(height, cameraDeviceId);
|
||||
|
||||
// if there is only video, switch to the new camera stream.
|
||||
} else {
|
||||
createLocalTracksF({
|
||||
devices: [ 'video' ],
|
||||
cameraDeviceId,
|
||||
micDeviceId: null
|
||||
})
|
||||
.then(([ stream ]) => {
|
||||
// if we are in audio only mode or video was muted before
|
||||
// changing device, then mute
|
||||
if (this.isAudioOnly() || videoWasMuted) {
|
||||
return stream.mute()
|
||||
.then(() => stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
})
|
||||
.then(stream => this.useVideoStream(stream))
|
||||
.then(() => {
|
||||
logger.log('switched local video device');
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => APP.store.dispatch(notifyCameraError(err)));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2251,6 +2407,13 @@ export default {
|
||||
cameraDeviceId: this.localVideo.getDeviceId()
|
||||
}));
|
||||
}
|
||||
|
||||
// If screenshare is in progress, get the device id from the presenter track.
|
||||
if (this.localPresenterVideo) {
|
||||
APP.store.dispatch(updateSettings({
|
||||
cameraDeviceId: this.localPresenterVideo.getDeviceId()
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,6 +73,11 @@ var config = {
|
||||
// Disable measuring of audio levels.
|
||||
// disableAudioLevels: false,
|
||||
|
||||
// Enabling this will run the lib-jitsi-meet no audio detection module which
|
||||
// will notify the user if the current selected microphone has no audio
|
||||
// input and will suggest another valid device if one is present.
|
||||
// enableNoAudioDetection: false
|
||||
|
||||
// Start the conference in audio only mode (no video is being received nor
|
||||
// sent).
|
||||
// startAudioOnly: false,
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
.avatar {
|
||||
align-items: center;
|
||||
background-color: #AAA;
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-weight: 100;
|
||||
justify-content: center;
|
||||
object-fit: cover;
|
||||
|
||||
&.avatar-small {
|
||||
height: 28px !important;
|
||||
width: 28px !important;
|
||||
}
|
||||
|
||||
&.avatar-xsmall {
|
||||
height: 16px !important;
|
||||
width: 16px !important;
|
||||
}
|
||||
|
||||
.jitsi-icon {
|
||||
transform: translateY(50%);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-foreign {
|
||||
@@ -28,4 +39,28 @@
|
||||
|
||||
.defaultAvatar {
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
.avatar-badge {
|
||||
position: relative;
|
||||
|
||||
&-available::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-available;
|
||||
}
|
||||
|
||||
&-away::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-away;
|
||||
}
|
||||
|
||||
&-busy::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-busy;
|
||||
}
|
||||
|
||||
&-idle::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-idle;
|
||||
}
|
||||
}
|
||||
@@ -192,4 +192,17 @@
|
||||
*/
|
||||
@mixin transparentBg($color, $alpha) {
|
||||
background-color: rgba(red($color), green($color), blue($color), $alpha);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar status badge mixin
|
||||
*/
|
||||
@mixin avatarBadge {
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 35%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ $defaultSideBarFontColor: #44A5FF;
|
||||
$defaultSemiDarkColor: #ACACAC;
|
||||
$defaultDarkColor: #2b3d5c;
|
||||
$defaultWarningColor: rgb(215, 121, 118);
|
||||
$presence-available: rgb(110, 176, 5);
|
||||
$presence-away: rgb(250, 201, 20);
|
||||
$presence-busy: rgb(233, 0, 27);
|
||||
$presence-idle: rgb(172, 172, 172);
|
||||
|
||||
/**
|
||||
* Toolbar
|
||||
|
||||
1
debian/control
vendored
@@ -37,6 +37,7 @@ Description: Configuration for web serving of Jitsi Meet
|
||||
Package: jitsi-meet-prosody
|
||||
Architecture: all
|
||||
Depends: openssl, prosody | prosody-trunk | prosody-0.11
|
||||
Replaces: jitsi-meet-tokens
|
||||
Description: Prosody configuration for Jitsi Meet
|
||||
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
|
||||
Videobridge to provide high quality, scalable video conferences.
|
||||
|
||||
1
debian/jitsi-meet-prosody.docs
vendored
@@ -1,2 +1 @@
|
||||
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example
|
||||
doc/debian/jitsi-meet-prosody/README
|
||||
|
||||
2
debian/jitsi-meet-prosody.install
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example /usr/share/jitsi-meet-prosody/
|
||||
resources/prosody-plugins/ /usr/share/jitsi-meet/
|
||||
4
debian/jitsi-meet-prosody.postinst
vendored
@@ -93,7 +93,7 @@ case "$1" in
|
||||
PROSODY_CONFIG_PRESENT="false"
|
||||
mkdir -p /etc/prosody/conf.avail/
|
||||
mkdir -p /etc/prosody/conf.d/
|
||||
cp /usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
|
||||
cp /usr/share/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
|
||||
sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/jitmeetSecret/$JVB_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/focusSecret/$JICOFO_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
@@ -179,7 +179,7 @@ case "$1" in
|
||||
fi
|
||||
|
||||
if [ "$PROSODY_CONFIG_PRESENT" = "false" ]; then
|
||||
invoke-rc.d prosody restart
|
||||
invoke-rc.d prosody restart || true
|
||||
fi
|
||||
;;
|
||||
|
||||
|
||||
1
debian/jitsi-meet-tokens.install
vendored
@@ -1 +0,0 @@
|
||||
resources/prosody-plugins/ /usr/share/jitsi-meet/
|
||||
2
debian/jitsi-meet-tokens.postinst
vendored
@@ -78,7 +78,7 @@ case "$1" in
|
||||
fi
|
||||
|
||||
if [ -x "/etc/init.d/prosody" ]; then
|
||||
invoke-rc.d prosody restart
|
||||
invoke-rc.d prosody restart || true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
|
||||
2
debian/jitsi-meet-tokens.postrm
vendored
@@ -41,7 +41,7 @@ case "$1" in
|
||||
sed -i 's/authentication = "token"/authentication = "anonymous"/g' $PROSODY_HOST_CONFIG
|
||||
sed -i "s/ app_id=\"$APP_ID\"/ --app_id=\"example_app_id\"/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/ app_secret=\"$APP_SECRET\"/ --app_secret=\"example_app_secret\"/g" $PROSODY_HOST_CONFIG
|
||||
sed -i 's/ modules_enabled = { "token_verification" }/ --modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG
|
||||
sed -i 's/ -- "token_verification"/ "token_verification"/g' $PROSODY_HOST_CONFIG
|
||||
|
||||
if [ -x "/etc/init.d/prosody" ]; then
|
||||
invoke-rc.d prosody restart
|
||||
|
||||
3
debian/jitsi-meet-web-config.docs
vendored
@@ -1,4 +1 @@
|
||||
doc/debian/jitsi-meet/jitsi-meet.example
|
||||
doc/debian/jitsi-meet/jitsi-meet.example-apache
|
||||
doc/debian/jitsi-meet/README
|
||||
config.js
|
||||
|
||||
3
debian/jitsi-meet-web-config.install
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
doc/debian/jitsi-meet/jitsi-meet.example /usr/share/jitsi-meet-web-config/
|
||||
doc/debian/jitsi-meet/jitsi-meet.example-apache /usr/share/jitsi-meet-web-config/
|
||||
config.js /usr/share/jitsi-meet-web-config/
|
||||
14
debian/jitsi-meet-web-config.postinst
vendored
@@ -98,7 +98,9 @@ case "$1" in
|
||||
# jitsi meet
|
||||
JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
|
||||
if [ ! -f $JITSI_MEET_CONFIG ] ; then
|
||||
cp /usr/share/doc/jitsi-meet-web-config/config.js $JITSI_MEET_CONFIG
|
||||
cp /usr/share/jitsi-meet-web-config/config.js $JITSI_MEET_CONFIG
|
||||
# replaces needed config for multidomain as it works only with nginx
|
||||
sed -i "s/conference.jitsi-meet.example.com/conference.<\!--# echo var=\"subdomain\" default=\"\" -->jitsi-meet.example.com/g" $JITSI_MEET_CONFIG
|
||||
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $JITSI_MEET_CONFIG
|
||||
fi
|
||||
|
||||
@@ -167,7 +169,7 @@ case "$1" in
|
||||
|
||||
db_set jitsi-meet/jvb-serve "true"
|
||||
|
||||
invoke-rc.d jitsi-videobridge restart
|
||||
invoke-rc.d jitsi-videobridge restart || true
|
||||
elif [[ "$FORCE_NGINX" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
|
||||
# this is a reconfigure, lets just delete old links
|
||||
if [ "$RECONFIGURING" = "true" ] ; then
|
||||
@@ -177,7 +179,7 @@ case "$1" in
|
||||
|
||||
# nginx conf
|
||||
if [ ! -f /etc/nginx/sites-available/$JVB_HOSTNAME.conf ] ; then
|
||||
cp /usr/share/doc/jitsi-meet-web-config/jitsi-meet.example /etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
cp /usr/share/jitsi-meet-web-config/jitsi-meet.example /etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
if [ ! -f /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf ] ; then
|
||||
ln -s /etc/nginx/sites-available/$JVB_HOSTNAME.conf /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
@@ -196,7 +198,7 @@ case "$1" in
|
||||
/etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
|
||||
invoke-rc.d nginx reload
|
||||
invoke-rc.d nginx reload || true
|
||||
elif [[ "$FORCE_APACHE" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
|
||||
# this is a reconfigure, lets just delete old links
|
||||
if [ "$RECONFIGURING" = "true" ] ; then
|
||||
@@ -208,7 +210,7 @@ case "$1" in
|
||||
if [ ! -f /etc/apache2/sites-available/$JVB_HOSTNAME.conf ] ; then
|
||||
# when creating new config, make sure all needed modules are enabled
|
||||
a2enmod rewrite ssl headers proxy_http include
|
||||
cp /usr/share/doc/jitsi-meet-web-config/jitsi-meet.example-apache /etc/apache2/sites-available/$JVB_HOSTNAME.conf
|
||||
cp /usr/share/jitsi-meet-web-config/jitsi-meet.example-apache /etc/apache2/sites-available/$JVB_HOSTNAME.conf
|
||||
a2ensite $JVB_HOSTNAME.conf
|
||||
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /etc/apache2/sites-available/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
@@ -225,7 +227,7 @@ case "$1" in
|
||||
/etc/apache2/sites-available/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
|
||||
invoke-rc.d apache2 reload
|
||||
invoke-rc.d apache2 reload || true
|
||||
fi
|
||||
|
||||
echo "----------------"
|
||||
|
||||
2
debian/rules
vendored
@@ -14,7 +14,7 @@ override_dh_auto_build:
|
||||
|
||||
override_dh_install: $(LANGUAGES)
|
||||
dh_installdirs
|
||||
dh_install -X/config.js -X/package.json
|
||||
dh_install
|
||||
|
||||
$(LANGUAGES):
|
||||
LOCALE=$$(echo $@ | cut -c1-2) ; \
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
-- Plugins path gets uncommented during jitsi-meet-tokens package install - that's where token plugin is located
|
||||
--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
|
||||
plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
|
||||
|
||||
-- domain mapper options, must at least have domain base set to use the mapper
|
||||
muc_mapper_domain_base = "jitmeet.example.com";
|
||||
|
||||
VirtualHost "jitmeet.example.com"
|
||||
-- enabled = false -- Remove this line to enable this host
|
||||
@@ -16,18 +18,23 @@ VirtualHost "jitmeet.example.com"
|
||||
key = "/etc/prosody/certs/jitmeet.example.com.key";
|
||||
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
|
||||
}
|
||||
speakerstats_component = "speakerstats.jitmeet.example.com"
|
||||
-- we need bosh
|
||||
modules_enabled = {
|
||||
"bosh";
|
||||
"pubsub";
|
||||
"ping"; -- Enable mod_ping
|
||||
"speakerstats";
|
||||
}
|
||||
|
||||
c2s_require_encryption = false
|
||||
|
||||
Component "conference.jitmeet.example.com" "muc"
|
||||
storage = "null"
|
||||
--modules_enabled = { "token_verification" }
|
||||
modules_enabled = {
|
||||
"muc_meeting_id";
|
||||
"muc_domain_mapper";
|
||||
-- "token_verification";
|
||||
}
|
||||
admins = { "focusUser@auth.jitmeet.example.com" }
|
||||
|
||||
Component "jitsi-videobridge.jitmeet.example.com"
|
||||
@@ -38,3 +45,6 @@ VirtualHost "auth.jitmeet.example.com"
|
||||
|
||||
Component "focus.jitmeet.example.com"
|
||||
component_secret = "focusSecret"
|
||||
|
||||
Component "speakerstats.jitmeet.example.com" "speakerstats_component"
|
||||
muc_component = "conference.jitmeet.example.com"
|
||||
|
||||
@@ -19,7 +19,11 @@ server {
|
||||
ssl_certificate_key /etc/jitsi/meet/jitsi-meet.example.com.key;
|
||||
|
||||
root /usr/share/jitsi-meet;
|
||||
|
||||
# ssi on with javascript for multidomain variables in config.js
|
||||
ssi on;
|
||||
ssi_types application/x-javascript application/javascript;
|
||||
|
||||
index index.html index.htm;
|
||||
error_page 404 /static/404.html;
|
||||
|
||||
@@ -52,4 +56,28 @@ server {
|
||||
location @root_path {
|
||||
rewrite ^/(.*)$ / break;
|
||||
}
|
||||
|
||||
location ~ ^/([^/?&:'"]+)/config.js$
|
||||
{
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
|
||||
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
|
||||
}
|
||||
|
||||
#Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
|
||||
location ~ ^/([^/?&:'"]+)/(.*)$ {
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
rewrite ^/([^/?&:'"]+)/(.*)$ /$2;
|
||||
}
|
||||
|
||||
# BOSH for subdomains
|
||||
location ~ ^/([^/?&:'"]+)/http-bind {
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
set $prefix "$1";
|
||||
|
||||
rewrite ^/(.*)$ /http-bind;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@ server {
|
||||
rewrite ^/(.*)$ / break;
|
||||
}
|
||||
|
||||
location / {
|
||||
ssi on;
|
||||
}
|
||||
|
||||
location ~ ^/([^/?&:'"]+)/config.js$
|
||||
{
|
||||
set $subdomain "$1.";
|
||||
@@ -64,4 +60,4 @@ server {
|
||||
rewrite ^/(.*)$ /http-bind;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,6 @@ modules_enabled = {
|
||||
"adhoc";
|
||||
"websocket";
|
||||
"http_altconnect";
|
||||
-- include domain mapper as global level module
|
||||
"muc_domain_mapper";
|
||||
}
|
||||
|
||||
-- domain mapper options, must at least have domain base set to use the mapper
|
||||
|
||||
BIN
images/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="324px" height="63.8px" viewBox="0 0 324 63.8" style="enable-background:new 0 0 324 63.8;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#0061FF;}
|
||||
.st1{display:none;}
|
||||
.st2{display:inline;}
|
||||
.st3{fill:none;}
|
||||
</style>
|
||||
<path class="st0" d="M37.6,12L18.8,24l18.8,12L18.8,48L0,35.9l18.8-12L0,12L18.8,0L37.6,12z M18.7,51.8l18.8-12l18.8,12l-18.8,12
|
||||
L18.7,51.8z M37.6,35.9l18.8-12L37.6,12L56.3,0l18.8,12L56.3,24l18.8,12L56.3,48L37.6,35.9z"/>
|
||||
<path d="M89.8,12H105c9.7,0,17.7,5.6,17.7,18.4v2.7c0,12.9-7.5,18.7-17.4,18.7H89.8V12z M98.3,19.2v25.3h6.5c5.5,0,9.2-3.6,9.2-11.6
|
||||
v-2.1c0-8-3.9-11.6-9.5-11.6H98.3z M127.2,19.6h6.8l1.1,7.5c1.3-5.1,4.6-7.8,10.6-7.8h2.1v8.6h-3.5c-6.9,0-8.6,2.4-8.6,9.2v14.8
|
||||
h-8.4V19.6H127.2z M149.5,36.4v-0.9c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.5,16.3-16.3,16.3
|
||||
C155.4,52.6,149.5,47,149.5,36.4z M173.5,36.3v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1
|
||||
C170.5,45.3,173.5,42.1,173.5,36.3z M186.5,19.6h7l0.8,6.1c1.7-4.1,5.3-6.9,10.6-6.9c8.2,0,13.6,5.9,13.6,16.8v0.9
|
||||
c0,10.6-6,16.2-13.6,16.2c-5.1,0-8.6-2.3-10.3-6V63h-8.2L186.5,19.6L186.5,19.6z M210,36.3v-0.7c0-6.4-3.3-9.6-7.7-9.6
|
||||
c-4.7,0-7.8,3.6-7.8,9.6v0.6c0,5.7,3,9.3,7.7,9.3C207,45.4,210,42.3,210,36.3z M230.9,45.9l-0.7,5.9H223v-43h8.2v16.5
|
||||
c1.8-4.2,5.4-6.5,10.5-6.5c7.7,0.1,13.4,5.4,13.4,16.1v1c0,10.7-5.4,16.8-13.6,16.8C236.1,52.6,232.6,50.1,230.9,45.9z M246.5,35.9
|
||||
v-0.8c0-5.9-3.2-9.2-7.7-9.2c-4.6,0-7.8,3.7-7.8,9.3v0.7c0,6,3.1,9.5,7.7,9.5C243.6,45.4,246.5,42.3,246.5,35.9z M258.7,36.4v-0.9
|
||||
c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.6,16.3-16.3,16.3C264.6,52.6,258.7,47,258.7,36.4z M282.8,36.3
|
||||
v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1C279.8,45.3,282.8,42.1,282.8,36.3z M302.3,35.1L291,19.6
|
||||
h9.7l6.5,9.7l6.6-9.7h9.6L311.9,35L324,51.8h-9.5l-7.4-10.7l-7.2,10.7H290L302.3,35.1z"/>
|
||||
<g id="Editble" class="st1">
|
||||
<g class="st2">
|
||||
<rect x="-105" y="5" class="st3" width="506" height="71.8"/>
|
||||
<path d="M0.2,13.6h16.3c10.4,0,19,6.1,19,19.8v2.9c0,13.8-8,20-18.7,20H0.2V13.6z M9.4,21.3v27.2h7c5.9,0,9.9-3.9,9.9-12.5v-2.2
|
||||
c0-8.6-4.1-12.5-10.2-12.5H9.4z M40.4,21.8h7.3l1.1,8c1.4-5.5,4.9-8.3,11.3-8.3h2.2v9.2h-3.7c-7.4,0-9.2,2.6-9.2,9.9v15.8h-9
|
||||
C40.4,56.4,40.4,21.8,40.4,21.8z M64.3,39.8v-1c0-11.6,7.4-17.9,17.5-17.9c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7,17.5-17.5,17.5
|
||||
C70.6,57.3,64.3,51.2,64.3,39.8z M90.1,39.7v-0.8c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7
|
||||
C86.9,49.4,90.1,46,90.1,39.7z M104,21.8h7.6l0.9,6.6c1.9-4.4,5.7-7.4,11.4-7.4c8.8,0,14.6,6.4,14.6,18v1
|
||||
c0,11.4-6.4,17.3-14.6,17.3c-5.5,0-9.2-2.5-11-6.5v17.5H104V21.8z M129.3,39.8V39c0-6.9-3.5-10.3-8.3-10.3c-5,0-8.4,3.8-8.4,10.3
|
||||
v0.7c0,6.1,3.2,10,8.2,10C126,49.5,129.3,46.1,129.3,39.8z M151.7,50.1l-0.7,6.3h-7.8V10.2h8.8V28c1.9-4.5,5.8-7,11.2-7
|
||||
c8.2,0.1,14.3,5.8,14.3,17.3v1c0,11.5-5.8,18-14.6,18C157.3,57.3,153.5,54.5,151.7,50.1z M168.5,39.3v-0.8c0-6.4-3.5-9.8-8.3-9.8
|
||||
c-5,0-8.4,4-8.4,10v0.7c0,6.5,3.3,10.2,8.3,10.2C165.3,49.5,168.5,46.1,168.5,39.3z M181.6,39.8v-1c0-11.6,7.4-17.9,17.5-17.9
|
||||
c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7.1,17.5-17.5,17.5C187.9,57.3,181.6,51.2,181.6,39.8z M207.4,39.7v-0.8
|
||||
c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7C204.2,49.4,207.4,46,207.4,39.7z M228.3,38.4
|
||||
l-12.1-16.7h10.4l7,10.4l7.1-10.4H251l-12.3,16.6l13,18h-10.2l-8-11.5l-7.7,11.5h-10.6L228.3,38.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
0
images/dropboxLogo_square.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 15 KiB |
BIN
images/ie.png
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,182 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 744.09448819 1052.3622047"
|
||||
id="svg3526"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
inkscape:export-filename="/Users/ystamcheva/Dropbox/Designs/appLogo.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
sodipodi:docname="logo-blue.svg">
|
||||
<defs
|
||||
id="defs3528" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="817.30793"
|
||||
inkscape:cy="496.00851"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="851"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata3531">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g4181">
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g33">
|
||||
<path
|
||||
d="m 257.311,591.057 c 27.544,0 85.707,-16.445 124.179,-27.332 5.595,-1.575 10.81,-3.049 15.549,-4.371 0.767,-0.211 1.51,-0.403 2.213,-0.579 -2.161,-2.139 -5.755,-5.387 -11.612,-10.295 -25.628,-17.369 -49.827,-25.456 -76.146,-25.456 -5.741,0 -11.707,0.352 -18.208,1.088 -22.283,2.535 -40.848,7.845 -49.767,10.39 -4.521,1.296 -5.883,1.683 -7.292,1.683 -2.688,0 -4.997,-1.599 -5.9,-4.069 -0.904,-2.483 -0.13,-5.223 1.969,-6.981 l 0.127,-0.102 c 15.379,-12.883 44.032,-36.866 98.39,-47.582 9.428,-1.853 19.514,-2.796 29.968,-2.796 24.334,0 49.53,5.026 74.869,14.925 34.511,13.474 58.094,30.771 77.062,44.67 10.211,7.489 19.03,13.959 26.705,17.516 1.961,0.912 2.979,1.169 3.453,1.236 0.349,-0.452 1.106,-1.7 2.219,-4.974 0.298,-0.867 2.453,-10.019 -13.007,-62.071 -8.985,-30.217 -19.822,-61.077 -25.465,-74.778 -10.916,-26.509 -8.237,-45.296 -4.877,-56.284 -9.248,3.399 -18.701,8.688 -28.646,15.993 l -0.62,0.458 c -4.969,3.684 -10.031,7.853 -15.482,12.725 -32.074,28.718 -56.104,43.69 -71.455,44.504 l -0.423,0.021 -0.421,-0.036 c -13.524,-1.148 -34.019,-20.834 -42.403,-30.801 -1.743,-1.169 -3.729,-1.699 -6.35,-1.699 -2.632,0 -5.583,0.553 -8.438,1.095 -2.077,0.394 -4.218,0.795 -6.341,1.01 -6.767,0.679 -16.252,2.867 -25.406,4.974 -4.413,1.014 -8.967,2.063 -13.13,2.922 -0.079,0.013 -1.866,0.382 -5.06,1.224 -22.624,6.693 -39.673,14.372 -48.012,21.628 -0.091,0.079 -0.36,0.288 -0.789,0.603 -5.64,4.009 -19.199,15.447 -23.29,34.907 l -0.043,0.162 c -8.541,35.837 4.408,80.28 21.615,105.666 8.093,11.932 16.814,19.376 23.944,20.42 1.775,0.252 3.905,0.386 6.321,0.386 z"
|
||||
id="path35"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g37">
|
||||
<path
|
||||
d="m 383.729,400.995 c 0.549,0.108 1.191,0.162 1.9,0.162 14.785,0 47.804,-21.408 53.912,-31.205 l 0.486,-0.78 0.694,-0.611 c 2.083,-2.056 8.099,-12.885 11.019,-19.367 -31.312,-9.394 -34.767,-26.347 -37.821,-41.41 -0.355,-1.749 -0.667,-3.324 -0.946,-4.732 -0.357,-1.842 -0.731,-3.713 -1.052,-5.159 -46.646,15.471 -60.905,24.154 -68.687,30.611 -4.027,3.345 -6.398,12.858 5.215,39.189 5.932,13.422 26.386,31.591 35.28,33.302 z"
|
||||
id="path39"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path43"
|
||||
d="m 183.03568,710.19014 c -5.799,-6.834 -8.258,-15.447 -7.293,-25.624 4.105,-49.397 -1.525,-61.33 -4.132,-64.162 -0.629,-0.685 -0.969,-0.685 -1.238,-0.685 -0.101,0 -0.195,0.006 -0.296,0.016 -4.84,1.157 -37.441,23.198 -44.638,89.005 -3.471,31.758 2.611,72.542 7.794,97.348 4.165,-14.646 10.742,-30.779 23.483,-47.384 11.862,-15.444 24.801,-27.623 40.852,-38.298 -4.99,-2.075 -10.346,-5.274 -14.532,-10.216 z" />
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g45">
|
||||
<path
|
||||
d="m 485.028,154.141 c -3.896,25.701 -10.239,50.115 -22.077,75.883 12.904,-14.609 20.445,-30.481 22.971,-48.296 1.051,-7.38 2.045,-14.439 -0.894,-27.587 z"
|
||||
id="path47"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g49">
|
||||
<path
|
||||
d="m 413.102,273.797 c 23.135,-20.915 37.22,-55.455 43.078,-75.971 -20.149,19.407 -44.636,29.82 -60.351,36.512 -5.412,2.308 -10.08,4.295 -12.878,5.926 -1.178,0.685 -2.367,1.374 -3.571,2.069 -9.533,5.515 -23.924,13.85 -26.022,18.987 l -0.06,0.167 -0.078,0.165 c -6.529,13.72 -10.208,34.352 -11.387,46.184 15.135,-9.242 30.738,-15.41 43.699,-20.529 12.03,-4.753 22.432,-8.863 27.57,-13.51 z"
|
||||
id="path51"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g53">
|
||||
<path
|
||||
d="m 436.439,291.877 c -0.141,0.357 -0.292,0.695 -0.455,1.017 -3.833,11.143 1.446,26.3 11.227,32.017 2.602,1.522 5.132,2.452 7.559,2.772 0.334,0.014 0.666,0.027 1.001,0.027 7.601,0 13.801,-5.56 18.4,-16.519 2.896,-8.34 3.308,-18.23 1.125,-27.158 -1.696,-6.936 -6.084,-15.215 -8.88,-19.343 -5.219,3.582 -15.533,11.462 -22.615,17.716 -4.946,4.777 -6.733,7.785 -7.362,9.471 z"
|
||||
id="path55"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g57">
|
||||
<path
|
||||
d="m 501.845,575.103 c 8.403,-2.29 15.076,-4.165 19.998,-5.623 -10.137,-7.061 -21.871,-15.846 -37.823,-28.253 -39.096,-30.404 -81.019,-45.826 -124.587,-45.826 -23.861,0 -44.647,4.592 -61.098,10.151 4.101,-0.255 8.271,-0.377 12.554,-0.377 5.088,0 10.42,0.179 15.842,0.541 16.949,1.136 60.616,8.845 100.106,55.931 7.956,9.469 16.507,17.307 40.828,17.307 8.679,0 18.796,-0.967 30.913,-2.959 0.749,-0.209 1.882,-0.518 3.267,-0.892 z"
|
||||
id="path59"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g61">
|
||||
<path
|
||||
d="m 557.268,369.949 c -7.755,-12.043 -17.498,-19.524 -25.41,-19.524 -1.464,0 -2.862,0.258 -4.154,0.765 -4.239,1.672 -10.952,21.042 -7.979,35.126 2.023,9.582 13.67,41.96 19.262,57.52 2.142,5.958 3.18,8.869 3.527,9.951 0.275,0.853 0.67,2.077 1.17,3.621 4.517,13.765 16.111,49.145 19.562,77.793 7.175,-30.554 11.239,-67.36 9.647,-111.409 -0.723,-20.199 -6.274,-39.323 -15.625,-53.843 z"
|
||||
id="path63"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g65">
|
||||
<path
|
||||
d="m 412.08,575.289 c -0.153,-0.2 -0.3,-0.397 -0.445,-0.585 -0.614,0.1 -1.616,0.319 -3.185,0.776 l -0.657,0.197 c -8.011,2.95 -22.707,7.908 -39.694,13.64 -20.387,6.87 -43.477,14.659 -62.808,21.595 -24.596,9.165 -32.572,12.781 -35.073,14.048 -0.454,1.218 -0.963,2.772 -1.53,4.486 -5.817,17.705 -19.139,58.23 -84.831,86.562 13.568,13.744 43.101,38.415 101.24,38.415 5.035,0 10.258,-0.188 15.494,-0.566 43.896,-3.121 85.158,-22.544 116.206,-54.673 28.233,-29.21 44.259,-65.641 44.507,-100.76 -6.871,-0.571 -18.519,-2.281 -29.301,-7.4 -0.125,-0.061 -12.447,-6.002 -19.923,-15.735 z"
|
||||
id="path67"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g69">
|
||||
<path
|
||||
d="m 162.104,639.109 c -0.122,10.334 -1.489,20.245 -2.82,29.907 -0.716,5.216 -1.464,10.615 -2.014,16.041 -0.746,10.914 1.612,14.717 2.659,15.829 0.571,0.629 1.513,1.346 3.536,1.346 1.558,0 3.418,-0.432 5.383,-1.251 19.507,-8.176 38.032,-22.367 46.937,-30.243 -13.668,-6.095 -34.689,-19.26 -53.681,-31.629 z"
|
||||
id="path71"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g73">
|
||||
<path
|
||||
d="m 484.26,598.224 c -0.552,7.258 -1.737,20.949 -3.631,31.378 -2.295,12.629 -6.095,23.31 -8.305,28.889 3.945,3.648 7.878,7.228 10.429,9.488 10.265,-6.718 43.961,-32.297 67.208,-90.368 -7.447,5.03 -17.906,9.456 -31.465,13.332 -13.797,3.929 -27.204,6.229 -34.236,7.281 z"
|
||||
id="path75"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#17a0db;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(43.272677,-6.8248629)"
|
||||
id="g85"
|
||||
style="fill:#17a0db;fill-opacity:1">
|
||||
<path
|
||||
style="fill:#17a0db;fill-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path87"
|
||||
d="M 627.562,350.519 C 613.5,321.124 593.893,306.283 580.351,297.677 568.965,290.444 555.872,285.188 541.339,282 c -1.622,-10.158 -4.397,-20.542 -8.198,-30.646 24.507,-36.531 30.407,-77.605 17.008,-119.213 C 539.858,100.151 531.868,79.524 524.996,67.213 510.15,40.585 489.58,34.997 474.936,34.997 c -15.09,0 -29.538,6.412 -39.667,17.61 -10.37,11.462 -15.213,26.462 -13.634,42.228 1.349,13.446 -2.178,37.872 -4.519,46.594 -0.04,0.117 -4.202,11.776 -35.168,26.784 -0.746,0.268 -2.332,0.811 -4.773,1.629 -17.812,5.965 -50.913,17.062 -72.963,46.219 -16.847,20.407 -20.985,40.629 -25.766,64.036 -2.858,13.955 -5.846,32.187 -5.105,53.745 -55.35,12.291 -95.226,37.338 -118.609,74.54 -24.203,38.52 -28.402,86.272 -12.468,141.993 l 0.14,0.414 c 0.292,1.014 0.6,2.024 0.921,3.03 -2.718,-0.466 -5.465,-0.858 -8.285,-1.169 -2.469,-0.284 -5.015,-0.42 -7.54,-0.42 -27.636,0 -57.043,17.371 -78.666,46.474 -16.427,22.098 -36.156,61.131 -36.852,121.593 -0.523,44.905 4.279,86.306 14.283,123.054 7.461,27.381 15.784,44.202 18.979,50.09 l 67.793,127.079 31.06,-140.731 c 10.6,-47.935 21.066,-68.283 34.571,-81.732 31.425,18.938 68.541,28.901 107.941,28.901 43.919,0 89.715,-12.667 128.934,-35.662 25.477,-14.954 47.193,-33.324 64.629,-54.658 0.236,0 0.469,0 0.704,0 l 1.857,-0.038 c 10.782,-0.365 25.522,-5.697 40.434,-14.63 12.421,-7.433 31.147,-21.108 49.946,-44.064 18.945,-23.155 34.402,-51.324 45.926,-83.731 13.5,-37.939 21.717,-82.115 24.404,-131.272 3.253,-45.723 -2.078,-83.533 -15.881,-112.384 z m -31.124,109.427 c -2.415,44.805 -9.745,84.66 -21.764,118.441 -9.713,27.302 -22.502,50.739 -38.005,69.69 -26.696,32.611 -52.783,41.355 -55.551,41.465 l -0.22,0 c -2.528,0 -4.012,-1.032 -11.095,-5.988 -1.979,-1.379 -4.969,-3.467 -7.436,-5.075 -14.813,28.811 -39.145,53.701 -70.659,72.185 -32.098,18.824 -69.432,29.202 -105.1,29.202 -42.352,0 -79.532,-13.979 -107.842,-40.493 -38.621,24.556 -61.833,45.044 -80.652,130.273 l -3.562,16.157 -7.787,-14.59 C 84.8,867.621 78.058,854.32 71.708,830.982 62.852,798.444 58.598,761.384 59.071,720.842 c 0.944,-80.909 44.373,-121.518 68.427,-121.518 0.792,0 1.578,0.039 2.328,0.128 22.8,2.551 37.699,12.402 64.745,30.291 2.796,1.853 5.74,3.8 8.843,5.838 9.69,6.36 23.387,14.125 26.835,14.791 6.562,-0.381 12.986,-15.079 14.853,-28.713 0.114,-0.829 0.226,-1.598 0.334,-2.315 0.147,-1.612 0.227,-3.03 0.27,-4.194 -1.144,-0.399 -2.333,-0.869 -3.547,-1.403 l -0.27,-0.091 c -17.012,-5.857 -41.868,-34.625 -54.378,-76.385 -12.081,-42.21 -9.691,-77.122 7.099,-103.83 27.221,-43.328 86.307,-53.515 105.849,-56.861 6.109,-1.214 12.498,-2.351 18.999,-3.378 3.035,-0.762 5.11,-1.399 6.449,-1.978 0.58,-0.403 0.835,-0.833 0.439,-2.403 l 0.53,-0.148 -0.513,0.115 c -0.237,-1.065 -0.565,-2.311 -0.941,-3.753 -0.521,-1.997 -1.103,-4.256 -1.705,-6.936 -6.05,-27.141 -2.962,-49.884 0.863,-68.559 4.297,-21.019 6.678,-32.656 16.605,-44.279 13.152,-18.103 36.803,-26.025 50.953,-30.77 3.948,-1.322 7.359,-2.462 9.331,-3.412 43.344,-20.789 57.145,-42.646 61.091,-57.318 3.127,-11.642 8.084,-42.253 5.931,-63.63 -0.239,-2.425 0.326,-4.421 1.695,-5.935 1.215,-1.341 2.942,-2.104 4.748,-2.104 4.061,0 9.623,0 30.377,64.478 10.949,33.996 2.785,65.868 -24.244,94.74 -0.347,0.375 -0.7,0.742 -1.04,1.095 -0.738,0.76 -1.848,1.909 -1.999,2.326 0.006,0 -0.048,1.042 1.755,4.031 11.425,18.864 17.633,42.323 15.832,59.763 -0.429,4.062 -1.206,7.971 -1.879,11.411 -0.4,1.968 -0.879,4.377 -1.126,6.241 0.111,0 0.226,0 0.347,0 3.088,-0.327 7.867,-0.7 13.628,-0.7 13.556,0 32.969,2.077 48.503,11.951 9.382,5.952 21.255,15.137 29.981,33.404 10.281,21.472 14.096,51.453 11.369,89.114 z" />
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
style="fill:#ffffff"
|
||||
d=""
|
||||
id="path3618"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 14 KiB |
BIN
images/opera.png
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width='20px' height='20px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-spin"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><g transform="translate(50 50)"><g transform="rotate(0) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(45) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.12s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.12s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(90) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.25s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.25s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(135) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.37s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.37s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(180) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.5s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.5s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(225) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.62s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.62s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(270) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.75s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.75s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g><g transform="rotate(315) translate(34 0)"><circle cx="0" cy="0" r="8" fill="#ffffff"><animate attributeName="opacity" from="1" to="0.1" begin="0.87s" dur="1s" repeatCount="indefinite"></animate><animateTransform attributeName="transform" type="scale" from="1.5" to="1" begin="0.87s" dur="1s" repeatCount="indefinite"></animateTransform></circle></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,64 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="480"
|
||||
height="270"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.2 r9819"
|
||||
sodipodi:docname="videomask.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-16.428571"
|
||||
inkscape:cy="520"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="998"
|
||||
inkscape:window-height="711"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-782.36218)">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.92795467;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="rect2985"
|
||||
width="479.07202"
|
||||
height="269.07205"
|
||||
x="0.46397734"
|
||||
y="782.82617"
|
||||
ry="20" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--#include virtual="base.html" -->
|
||||
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="css/all.css">
|
||||
|
||||
<script>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Script"
|
||||
scriptText = "exec > /tmp/${PROJECT_NAME}_archive.log 2>&1 UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal if [ "true" == ${ALREADYINVOKED:-false} ] then echo "RECURSION: Detected, stopping" else export ALREADYINVOKED="true" # make sure the output directory exists mkdir -p "${UNIVERSAL_OUTPUTFOLDER}" echo "Building for iPhoneSimulator" xcodebuild -workspace "${WORKSPACE_PATH}" -scheme "${TARGET_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8' ONLY_ACTIVE_ARCH=NO ARCHS='i386 x86_64' BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" ENABLE_BITCODE=YES OTHER_CFLAGS="-fembed-bitcode" BITCODE_GENERATION_MODE=bitcode build # Step 1. Copy the framework structure (from iphoneos build) to the universal folder echo "Copying to output folder" cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FULL_PRODUCT_NAME}" "${UNIVERSAL_OUTPUTFOLDER}/" # Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/." if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule" fi # Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory echo "Combining executables" lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${EXECUTABLE_PATH}" # Step 4. Create universal binaries for embedded frameworks #for SUB_FRAMEWORK in $( ls "${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks" ); do #BINARY_NAME="${SUB_FRAMEWORK%.*}" #lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SUB_FRAMEWORK}/${BINARY_NAME}" "${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}" #done # Step 5. Convenience step to copy the framework to the project's directory echo "Copying to project dir" yes | cp -Rf "${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}" "${PROJECT_DIR}" fi ">
|
||||
scriptText = "exec > /tmp/${PROJECT_NAME}_archive.log 2>&1 UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal if [ "true" == ${ALREADYINVOKED:-false} ] then echo "RECURSION: Detected, stopping" else export ALREADYINVOKED="true" # make sure the output directory exists mkdir -p "${UNIVERSAL_OUTPUTFOLDER}" echo "Building for iPhoneSimulator" xcodebuild -workspace "${WORKSPACE_PATH}" -scheme "${TARGET_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8' ONLY_ACTIVE_ARCH=NO ARCHS='x86_64' BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" ENABLE_BITCODE=YES OTHER_CFLAGS="-fembed-bitcode" BITCODE_GENERATION_MODE=bitcode build # Step 1. Copy the framework structure (from iphoneos build) to the universal folder echo "Copying to output folder" cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FULL_PRODUCT_NAME}" "${UNIVERSAL_OUTPUTFOLDER}/" # Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/." if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule" fi # Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory echo "Combining executables" lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${EXECUTABLE_PATH}" fi # Step 4. Convenience step to copy the framework to the project&apos;s directory echo "Copying to project dir&quot" yes | cp -Rf ${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME} ${PROJECT_DIR} ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
|
||||
@@ -630,6 +630,10 @@
|
||||
"lowerYourHand": "Lower your hand",
|
||||
"moreActions": "More actions",
|
||||
"mute": "Mute / Unmute",
|
||||
|
||||
"noAudioSignalTitle": "There is no input coming from your mic!",
|
||||
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider changing the device.",
|
||||
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider using the following device:",
|
||||
"openChat": "Open chat",
|
||||
"pip": "Enter Picture-in-Picture mode",
|
||||
"privateMessage": "Send private message",
|
||||
@@ -736,7 +740,7 @@
|
||||
"roomNameAllowedChars": "Meeting name should not contain any of these characters: ?, &, :, ', \", %, #.",
|
||||
"go": "GO",
|
||||
"goSmall": "GO",
|
||||
"join": "JOIN",
|
||||
"join": "CREATE / JOIN",
|
||||
"info": "Info",
|
||||
"privacy": "Privacy",
|
||||
"recentList": "Recent",
|
||||
|
||||
69
package-lock.json
generated
@@ -22,38 +22,6 @@
|
||||
"@atlaskit/type-helpers": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@atlaskit/avatar": {
|
||||
"version": "14.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/avatar/-/avatar-14.1.7.tgz",
|
||||
"integrity": "sha512-KGtV0lRr3g+JX3XLZQKDGxGhtbVFRvM/Ku5C+CEJw2uDl1KFY0dJxfr2a/E32bEgUuvmqSL7D3ROrTrlHJ2fMA==",
|
||||
"requires": {
|
||||
"@atlaskit/analytics-next": "^3.1.2",
|
||||
"@atlaskit/theme": "^7.0.1",
|
||||
"@atlaskit/tooltip": "^12.1.13",
|
||||
"@babel/runtime": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/analytics-next": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-3.1.2.tgz",
|
||||
"integrity": "sha512-bkYDvl3Ojsnim+bsc9BALfvOjiL7xdb2rTp/4yqUP9pfidtf5HudbOJ849+dKcRCmk/rFbfB/nhDBRU6rv1Ueg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"@atlaskit/theme": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-7.0.1.tgz",
|
||||
"integrity": "sha512-wxXDnkUablJketNCrQuNUuazufYEA7kv0Y6Yzv6uvqfuyNpWUQt4H1psz/MW8DbZmCdku9dEYbNVK3nFP5TDGg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@atlaskit/blanket": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/blanket/-/blanket-8.0.3.tgz",
|
||||
@@ -10931,8 +10899,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
|
||||
"from": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
|
||||
"version": "github:jitsi/lib-jitsi-meet#4e7034ee6a0d0e68487c583c981f4e07ab73926c",
|
||||
"from": "github:jitsi/lib-jitsi-meet#4e7034ee6a0d0e68487c583c981f4e07ab73926c",
|
||||
"requires": {
|
||||
"@jitsi/sdp-interop": "0.1.14",
|
||||
"@jitsi/sdp-simulcast": "0.2.2",
|
||||
@@ -14830,6 +14798,39 @@
|
||||
"jssha": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"react-native-collapsible": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-collapsible/-/react-native-collapsible-1.5.1.tgz",
|
||||
"integrity": "sha512-uQQ2s6l+7+L/pzJroisWsDsyVYVF5bQ+jGbLATNioRh/03SpEL8pcQEVKqVWswcNNR0B9GENixHaLzmuZIwpQg==",
|
||||
"requires": {
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"requires": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.8.1"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.12.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
|
||||
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-native-immersive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-immersive/-/react-native-immersive-2.0.0.tgz",
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"author": "",
|
||||
"readmeFilename": "README.md",
|
||||
"dependencies": {
|
||||
"@atlaskit/avatar": "14.1.7",
|
||||
"@atlaskit/button": "10.1.1",
|
||||
"@atlaskit/checkbox": "5.0.10",
|
||||
"@atlaskit/dropdown-menu": "6.1.25",
|
||||
@@ -57,7 +56,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#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#4e7034ee6a0d0e68487c583c981f4e07ab73926c",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.13",
|
||||
"moment": "2.19.4",
|
||||
@@ -72,6 +71,7 @@
|
||||
"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-collapsible": "1.5.1",
|
||||
"react-native-immersive": "2.0.0",
|
||||
"react-native-keep-awake": "4.0.0",
|
||||
"react-native-linear-gradient": "2.5.6",
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
host
|
||||
};
|
||||
|
||||
amplitude.getInstance(this._amplitudeOptions).init(amplitudeAPPKey);
|
||||
amplitude.getInstance(this._amplitudeOptions).init(amplitudeAPPKey, undefined, { includeReferrer: true });
|
||||
|
||||
if (user) {
|
||||
amplitude.getInstance(this._amplitudeOptions).setUserId(user);
|
||||
|
||||
@@ -35,9 +35,12 @@ class Amplitude {
|
||||
* Sets an identifier for the current user.
|
||||
*
|
||||
* @param {string} userId - The new user id.
|
||||
* @param {string} opt_userId - Currently not used.
|
||||
* @param {Object} opt_config - Currently not used.
|
||||
* @param {Function} opt_callback - Currently not used.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserId(userId) {
|
||||
setUserId(userId, opt_userId, opt_config, opt_callback) { // eslint-disable-line camelcase, no-unused-vars
|
||||
AmplitudeNative.setUserId(this._instanceName, userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -147,9 +147,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const state = getState();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
|
||||
if (localTracksDuration.conference.startedTime === -1) {
|
||||
if (localTracksDuration.conference.startedTime === -1 || action.mediaType === 'presenter') {
|
||||
// We don't want to track the media duration if the conference is not joined yet because otherwise we won't
|
||||
// be able to compare them with the conference duration (from conference join to conference will leave).
|
||||
// Also, do not track media duration for presenter tracks.
|
||||
break;
|
||||
}
|
||||
dispatch({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DialogContainer } from '../../base/dialog';
|
||||
import '../../base/user-interaction';
|
||||
import '../../chat';
|
||||
import '../../external-api';
|
||||
import '../../no-audio-signal';
|
||||
import '../../power-monitor';
|
||||
import '../../room-lock';
|
||||
import '../../talk-while-muted';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Text, TextInput, View } from 'react-native';
|
||||
import { connect as reduxConnect } from 'react-redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../base/color-scheme';
|
||||
import { toJid } from '../../base/connection';
|
||||
import { connect } from '../../base/connection/actions.native';
|
||||
import {
|
||||
@@ -19,7 +20,9 @@ import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet';
|
||||
import type { StyleType } from '../../base/styles';
|
||||
|
||||
import { authenticateAndUpgradeRole, cancelLogin } from '../actions';
|
||||
import styles from './styles';
|
||||
|
||||
// Register styles.
|
||||
import './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link LoginDialog}.
|
||||
@@ -58,6 +61,11 @@ type Props = {
|
||||
*/
|
||||
_progress: number,
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of this feature.
|
||||
*/
|
||||
_styles: StyleType,
|
||||
|
||||
/**
|
||||
* Redux store dispatch method.
|
||||
*/
|
||||
@@ -144,46 +152,10 @@ class LoginDialog extends Component<Props, State> {
|
||||
const {
|
||||
_connecting: connecting,
|
||||
_dialogStyles,
|
||||
_error: error,
|
||||
_progress: progress,
|
||||
_styles: styles,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
let messageKey;
|
||||
const messageOptions = {};
|
||||
|
||||
if (progress && progress < 1) {
|
||||
messageKey = 'connection.FETCH_SESSION_ID';
|
||||
} else if (error) {
|
||||
const { name } = error;
|
||||
|
||||
if (name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
|
||||
// Show a message that the credentials are incorrect only if the
|
||||
// credentials which have caused the connection to fail are the
|
||||
// ones which the user sees.
|
||||
const { credentials } = error;
|
||||
|
||||
if (credentials
|
||||
&& credentials.jid
|
||||
=== toJid(
|
||||
this.state.username,
|
||||
this.props._configHosts)
|
||||
&& credentials.password === this.state.password) {
|
||||
messageKey = 'dialog.incorrectPassword';
|
||||
}
|
||||
} else if (name) {
|
||||
messageKey = 'dialog.connectErrorWithMsg';
|
||||
messageOptions.msg = `${name} ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
const showMessage = messageKey || connecting;
|
||||
const message = messageKey
|
||||
? t(messageKey, messageOptions)
|
||||
: connecting
|
||||
? t('connection.CONNECTING')
|
||||
: '';
|
||||
|
||||
return (
|
||||
<CustomSubmitDialog
|
||||
okDisabled = { connecting }
|
||||
@@ -210,16 +182,77 @@ class LoginDialog extends Component<Props, State> {
|
||||
] }
|
||||
underlineColorAndroid = { FIELD_UNDERLINE }
|
||||
value = { this.state.password } />
|
||||
{ showMessage && (
|
||||
<Text style = { styles.dialogText }>
|
||||
{ message }
|
||||
</Text>
|
||||
) }
|
||||
{ this._renderMessage() }
|
||||
</View>
|
||||
</CustomSubmitDialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an optional message, if applicable.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderMessage() {
|
||||
const {
|
||||
_connecting: connecting,
|
||||
_error: error,
|
||||
_progress: progress,
|
||||
_styles: styles,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
let messageKey;
|
||||
let messageIsError = false;
|
||||
const messageOptions = {};
|
||||
|
||||
if (progress && progress < 1) {
|
||||
messageKey = 'connection.FETCH_SESSION_ID';
|
||||
} else if (error) {
|
||||
const { name } = error;
|
||||
|
||||
if (name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
|
||||
// Show a message that the credentials are incorrect only if the
|
||||
// credentials which have caused the connection to fail are the
|
||||
// ones which the user sees.
|
||||
const { credentials } = error;
|
||||
|
||||
if (credentials
|
||||
&& credentials.jid
|
||||
=== toJid(
|
||||
this.state.username,
|
||||
this.props._configHosts)
|
||||
&& credentials.password === this.state.password) {
|
||||
messageKey = 'dialog.incorrectPassword';
|
||||
messageIsError = true;
|
||||
}
|
||||
} else if (name) {
|
||||
messageKey = 'dialog.connectErrorWithMsg';
|
||||
messageOptions.msg = `${name} ${error.message}`;
|
||||
messageIsError = true;
|
||||
}
|
||||
} else if (connecting) {
|
||||
messageKey = 'connection.CONNECTING';
|
||||
}
|
||||
|
||||
if (messageKey) {
|
||||
const message = t(messageKey, messageOptions);
|
||||
const messageStyles = [
|
||||
styles.dialogText,
|
||||
messageIsError ? styles.errorMessage : styles.progressMessage
|
||||
];
|
||||
|
||||
return (
|
||||
<Text style = { messageStyles }>
|
||||
{ message }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_onUsernameChange: (string) => void;
|
||||
|
||||
/**
|
||||
@@ -295,14 +328,7 @@ class LoginDialog extends Component<Props, State> {
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: JitsiConference,
|
||||
* _configHosts: Object,
|
||||
* _connecting: boolean,
|
||||
* _dialogStyles: StyleType,
|
||||
* _error: Object,
|
||||
* _progress: number
|
||||
* }}
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const {
|
||||
@@ -323,7 +349,8 @@ function _mapStateToProps(state) {
|
||||
_configHosts: configHosts,
|
||||
_connecting: Boolean(connecting) || Boolean(thenableWithCancel),
|
||||
_error: connectionError || authenticateAndUpgradeRoleError,
|
||||
_progress: progress
|
||||
_progress: progress,
|
||||
_styles: ColorSchemeRegistry.get(state, 'LoginDialog')
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,41 @@
|
||||
import { BoxModel, ColorPalette, createStyleSheet } from '../../base/styles';
|
||||
|
||||
/**
|
||||
* The style common to {@code LoginDialog} and {@code WaitForOwnerDialog}.
|
||||
*/
|
||||
const dialog = {
|
||||
marginBottom: BoxModel.margin,
|
||||
marginTop: BoxModel.margin
|
||||
};
|
||||
|
||||
/**
|
||||
* The style common to {@code Text} rendered by {@code LoginDialog} and
|
||||
* {@code WaitForOwnerDialog}.
|
||||
*/
|
||||
const text = {
|
||||
color: ColorPalette.white
|
||||
};
|
||||
import { ColorSchemeRegistry, schemeColor } from '../../base/color-scheme';
|
||||
import { BoxModel } from '../../base/styles';
|
||||
|
||||
/**
|
||||
* The styles of the authentication feature.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
ColorSchemeRegistry.register('LoginDialog', {
|
||||
|
||||
/**
|
||||
* The style of {@code Text} rendered by the {@code Dialog}s of the
|
||||
* feature authentication.
|
||||
*/
|
||||
dialogText: {
|
||||
...text,
|
||||
margin: BoxModel.margin,
|
||||
marginTop: BoxModel.margin * 2
|
||||
},
|
||||
|
||||
/**
|
||||
* The style used when an error message is rendered.
|
||||
*/
|
||||
errorMessage: {
|
||||
color: schemeColor('errorText')
|
||||
},
|
||||
|
||||
/**
|
||||
* The style of {@code LoginDialog}.
|
||||
*/
|
||||
loginDialog: {
|
||||
...dialog,
|
||||
flex: 0,
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
marginBottom: BoxModel.margin,
|
||||
marginTop: BoxModel.margin
|
||||
},
|
||||
|
||||
/**
|
||||
* The style of {@code WaitForOwnerDialog}.
|
||||
* The style used then a progress message is rendered.
|
||||
*/
|
||||
waitForOwnerDialog: {
|
||||
...dialog,
|
||||
...text
|
||||
progressMessage: {
|
||||
color: schemeColor('text')
|
||||
}
|
||||
});
|
||||
|
||||
@@ -54,6 +54,11 @@ export type Props = {
|
||||
*/
|
||||
size: number,
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
status?: ?string,
|
||||
|
||||
/**
|
||||
* URL of the avatar, if any.
|
||||
*/
|
||||
@@ -117,6 +122,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
colorBase,
|
||||
id,
|
||||
size,
|
||||
status,
|
||||
url
|
||||
} = this.props;
|
||||
const { avatarFailed } = this.state;
|
||||
@@ -128,6 +134,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
initials: undefined,
|
||||
onAvatarLoadError: undefined,
|
||||
size,
|
||||
status,
|
||||
url: undefined
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ import styles from './styles';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
status?: ?string,
|
||||
|
||||
/**
|
||||
* External style passed to the componant.
|
||||
*/
|
||||
@@ -46,18 +51,40 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
styles.avatarContainer(size),
|
||||
style
|
||||
] }>
|
||||
{ avatar }
|
||||
<View>
|
||||
<View
|
||||
style = { [
|
||||
styles.avatarContainer(size),
|
||||
style
|
||||
] }>
|
||||
{ avatar }
|
||||
</View>
|
||||
{ this._renderAvatarStatus() }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_isIcon: (?string | ?Object) => boolean
|
||||
|
||||
/**
|
||||
* Renders a badge representing the avatar status.
|
||||
*
|
||||
* @returns {React$Elementaa}
|
||||
*/
|
||||
_renderAvatarStatus() {
|
||||
const { size, status } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { styles.badgeContainer }>
|
||||
<View style = { styles.badge(size, status) } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the default avatar.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ColorPalette } from '../../../styles';
|
||||
|
||||
const DEFAULT_SIZE = 65;
|
||||
@@ -27,6 +29,38 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
badge: (size: number = DEFAULT_SIZE, status: string) => {
|
||||
let color;
|
||||
|
||||
switch (status) {
|
||||
case 'available':
|
||||
color = 'rgb(110, 176, 5)';
|
||||
break;
|
||||
case 'away':
|
||||
color = 'rgb(250, 201, 20)';
|
||||
break;
|
||||
case 'busy':
|
||||
color = 'rgb(233, 0, 27)';
|
||||
break;
|
||||
case 'idle':
|
||||
color = 'rgb(172, 172, 172)';
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundColor: color,
|
||||
borderRadius: size / 2,
|
||||
bottom: 0,
|
||||
height: size * 0.3,
|
||||
position: 'absolute',
|
||||
width: size * 0.3
|
||||
};
|
||||
},
|
||||
|
||||
badgeContainer: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
},
|
||||
|
||||
initialsContainer: {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
|
||||
@@ -21,7 +21,12 @@ type Props = AbstractProps & {
|
||||
/**
|
||||
* ID of the component to be rendered.
|
||||
*/
|
||||
id?: string
|
||||
id?: string,
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
status?: ?string
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -40,29 +45,33 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
if (this._isIcon(url)) {
|
||||
return (
|
||||
<div
|
||||
className = { this._getAvatarClassName() }
|
||||
className = { `${this._getAvatarClassName()} ${this._getBadgeClassName()}` }
|
||||
id = { this.props.id }
|
||||
style = { this._getAvatarStyle(this.props.color) }>
|
||||
<Icon src = { url } />
|
||||
<Icon
|
||||
size = '50%'
|
||||
src = { url } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
return (
|
||||
<img
|
||||
className = { this._getAvatarClassName() }
|
||||
id = { this.props.id }
|
||||
onError = { this.props.onAvatarLoadError }
|
||||
src = { url }
|
||||
style = { this._getAvatarStyle() } />
|
||||
<div className = { this._getBadgeClassName() }>
|
||||
<img
|
||||
className = { this._getAvatarClassName() }
|
||||
id = { this.props.id }
|
||||
onError = { this.props.onAvatarLoadError }
|
||||
src = { url }
|
||||
style = { this._getAvatarStyle() } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (initials) {
|
||||
return (
|
||||
<div
|
||||
className = { this._getAvatarClassName() }
|
||||
className = { `${this._getAvatarClassName()} ${this._getBadgeClassName()}` }
|
||||
id = { this.props.id }
|
||||
style = { this._getAvatarStyle(this.props.color) }>
|
||||
<svg
|
||||
@@ -85,11 +94,13 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
|
||||
// default avatar
|
||||
return (
|
||||
<img
|
||||
className = { this._getAvatarClassName('defaultAvatar') }
|
||||
id = { this.props.id }
|
||||
src = { this.props.defaultAvatar || 'images/avatar.png' }
|
||||
style = { this._getAvatarStyle() } />
|
||||
<div className = { this._getBadgeClassName() }>
|
||||
<img
|
||||
className = { this._getAvatarClassName('defaultAvatar') }
|
||||
id = { this.props.id }
|
||||
src = { this.props.defaultAvatar || 'images/avatar.png' }
|
||||
style = { this._getAvatarStyle() } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,5 +131,20 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
return `avatar ${additional || ''} ${this.props.className || ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a class name to render a badge on the avatar, if necessary.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getBadgeClassName() {
|
||||
const { status } = this.props;
|
||||
|
||||
if (status) {
|
||||
return `avatar-badge avatar-badge-${status}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
_isIcon: (?string | ?Object) => boolean
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export default {
|
||||
// Generic app theme colors that are used accross the entire app.
|
||||
// All scheme definitions below inherit these values.
|
||||
background: 'rgb(255, 255, 255)',
|
||||
errorText: ColorPalette.red,
|
||||
icon: 'rgb(28, 32, 37)',
|
||||
text: 'rgb(28, 32, 37)'
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
getCurrentConference
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { MEDIA_TYPE } from '../media';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -589,7 +590,10 @@ function _syncReceiveVideoQuality({ getState }, next, action) {
|
||||
function _trackAddedOrRemoved(store, next, action) {
|
||||
const track = action.track;
|
||||
|
||||
if (track && track.local) {
|
||||
// TODO All track swapping should happen here instead of conference.js.
|
||||
// Since we swap the tracks for the web client in conference.js, ignore
|
||||
// presenter tracks here and do not add/remove them to/from the conference.
|
||||
if (track && track.local && track.mediaType !== MEDIA_TYPE.PRESENTER) {
|
||||
return (
|
||||
_syncConferenceLocalTracksWithState(store, action)
|
||||
.then(() => next(action)));
|
||||
|
||||
@@ -139,6 +139,39 @@ export function groupDevicesByKind(devices: Object[]): Object {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters audio devices from a list of MediaDeviceInfo objects.
|
||||
*
|
||||
* @param {Array<MediaDeviceInfo>} devices - Unfiltered media devices.
|
||||
* @private
|
||||
* @returns {Array<MediaDeviceInfo>} Filtered audio devices.
|
||||
*/
|
||||
export function filterAudioDevices(devices: Object[]): Object {
|
||||
return devices.filter(device => device.kind === 'audioinput');
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to strip any device details that are not very user friendly, like usb ids put in brackets at the end.
|
||||
*
|
||||
* @param {string} label - Device label to format.
|
||||
*
|
||||
* @returns {string} - Formatted string.
|
||||
*/
|
||||
export function formatDeviceLabel(label: string) {
|
||||
|
||||
let formattedLabel = label;
|
||||
|
||||
// Remove braked description at the end as it contains non user friendly strings i.e.
|
||||
// Microsoft® LifeCam HD-3000 (045e:0779:31dg:d1231)
|
||||
const ix = formattedLabel.lastIndexOf('(');
|
||||
|
||||
if (ix !== -1) {
|
||||
formattedLabel = formattedLabel.substr(0, ix);
|
||||
}
|
||||
|
||||
return formattedLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set device id of the audio output device which is currently in use.
|
||||
* Empty string stands for default device.
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from './actionTypes';
|
||||
import { showNotification, showWarningNotification } from '../../notifications';
|
||||
import { updateSettings } from '../settings';
|
||||
import { setAudioOutputDeviceId } from './functions';
|
||||
import { formatDeviceLabel, setAudioOutputDeviceId } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||
@@ -186,12 +186,7 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
|
||||
|
||||
// we want to strip any device details that are not very
|
||||
// user friendly, like usb ids put in brackets at the end
|
||||
let description = newDevice.label;
|
||||
const ix = description.lastIndexOf('(');
|
||||
|
||||
if (ix !== -1) {
|
||||
description = description.substr(0, ix);
|
||||
}
|
||||
const description = formatDeviceLabel(newDevice.label);
|
||||
|
||||
let titleKey;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { PureComponent, type Node } from 'react';
|
||||
import { SafeAreaView, ScrollView, View } from 'react-native';
|
||||
import { PanResponder, SafeAreaView, ScrollView, View } from 'react-native';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../color-scheme';
|
||||
import { SlidingView } from '../../../react';
|
||||
@@ -10,6 +10,16 @@ import { StyleType } from '../../../styles';
|
||||
|
||||
import { bottomSheetStyles as styles } from './styles';
|
||||
|
||||
/**
|
||||
* Minimal distance that needs to be moved by the finger to consider it a swipe.
|
||||
*/
|
||||
const GESTURE_DISTANCE_THRESHOLD = 5;
|
||||
|
||||
/**
|
||||
* The minimal speed needed to be achieved by the finger to consider it as a swipe.
|
||||
*/
|
||||
const GESTURE_SPEED_THRESHOLD = 0.2;
|
||||
|
||||
/**
|
||||
* The type of {@code BottomSheet}'s React {@code Component} prop types.
|
||||
*/
|
||||
@@ -29,13 +39,40 @@ type Props = {
|
||||
* Handler for the cancel event, which happens when the user dismisses
|
||||
* the sheet.
|
||||
*/
|
||||
onCancel: ?Function
|
||||
onCancel: ?Function,
|
||||
|
||||
/**
|
||||
* Callback to be attached to the custom swipe event of the BottomSheet.
|
||||
*/
|
||||
onSwipe?: Function,
|
||||
|
||||
/**
|
||||
* Function to render a bottom sheet header element, if necessary.
|
||||
*/
|
||||
renderHeader: ?Function
|
||||
};
|
||||
|
||||
/**
|
||||
* A component emulating Android's BottomSheet.
|
||||
*/
|
||||
class BottomSheet extends PureComponent<Props> {
|
||||
panResponder: Object;
|
||||
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.panResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: this._onShouldSetResponder.bind(this),
|
||||
onMoveShouldSetPanResponder: this._onShouldSetResponder.bind(this),
|
||||
onPanResponderRelease: this._onGestureEnd.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -43,7 +80,7 @@ class BottomSheet extends PureComponent<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _styles } = this.props;
|
||||
const { _styles, renderHeader } = this.props;
|
||||
|
||||
return (
|
||||
<SlidingView
|
||||
@@ -56,23 +93,66 @@ class BottomSheet extends PureComponent<Props> {
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.sheetAreaCover } />
|
||||
<View
|
||||
{ renderHeader && renderHeader() }
|
||||
<SafeAreaView
|
||||
style = { [
|
||||
styles.sheetItemContainer,
|
||||
_styles.sheet
|
||||
] }>
|
||||
<SafeAreaView>
|
||||
<ScrollView
|
||||
bounces = { false }
|
||||
showsVerticalScrollIndicator = { false } >
|
||||
{ this.props.children }
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
] }
|
||||
{ ...this.panResponder.panHandlers }>
|
||||
<ScrollView
|
||||
bounces = { false }
|
||||
showsVerticalScrollIndicator = { false }
|
||||
style = { styles.scrollView } >
|
||||
{ this.props.children }
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</SlidingView>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to handle a gesture end event.
|
||||
*
|
||||
* @param {Object} evt - The native gesture event.
|
||||
* @param {Object} gestureState - The gesture state.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onGestureEnd(evt, gestureState) {
|
||||
const verticalSwipe = Math.abs(gestureState.vy) > Math.abs(gestureState.vx)
|
||||
&& Math.abs(gestureState.vy) > GESTURE_SPEED_THRESHOLD;
|
||||
|
||||
if (verticalSwipe) {
|
||||
const direction = gestureState.vy > 0 ? 'down' : 'up';
|
||||
const { onCancel, onSwipe } = this.props;
|
||||
let isSwipeHandled = false;
|
||||
|
||||
if (onSwipe) {
|
||||
isSwipeHandled = onSwipe(direction);
|
||||
}
|
||||
|
||||
if (direction === 'down' && !isSwipeHandled) {
|
||||
// Swipe down is a special gesture that can be used to close the
|
||||
// BottomSheet, so if the swipe is not handled by the parent
|
||||
// component, we consider it as a request to close.
|
||||
onCancel && onCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the pan responder should activate, false otherwise.
|
||||
*
|
||||
* @param {Object} evt - The native gesture event.
|
||||
* @param {Object} gestureState - The gesture state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onShouldSetResponder({ nativeEvent }, gestureState) {
|
||||
return nativeEvent.touches.length === 1
|
||||
&& Math.abs(gestureState.dx) > GESTURE_DISTANCE_THRESHOLD
|
||||
&& Math.abs(gestureState.dy) > GESTURE_DISTANCE_THRESHOLD;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,6 +33,10 @@ export const bottomSheetStyles = {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
scrollView: {
|
||||
paddingHorizontal: MD_ITEM_MARGIN_PADDING
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the container of the sheet.
|
||||
*/
|
||||
@@ -44,9 +48,7 @@ export const bottomSheetStyles = {
|
||||
},
|
||||
|
||||
sheetItemContainer: {
|
||||
flex: -1,
|
||||
maxHeight: '60%',
|
||||
paddingHorizontal: MD_ITEM_MARGIN_PADDING
|
||||
flex: -1
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,23 +137,45 @@ export const inputDialog = {
|
||||
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
|
||||
*/
|
||||
ColorSchemeRegistry.register('BottomSheet', {
|
||||
/**
|
||||
* Style for the {@code Icon} element in a generic item of the menu.
|
||||
*/
|
||||
iconStyle: {
|
||||
color: schemeColor('icon'),
|
||||
fontSize: 24
|
||||
buttons: {
|
||||
/**
|
||||
* Style for the {@code Icon} element in a generic item of the menu.
|
||||
*/
|
||||
iconStyle: {
|
||||
color: schemeColor('icon'),
|
||||
fontSize: 24
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the label in a generic item rendered in the menu.
|
||||
*/
|
||||
labelStyle: {
|
||||
color: schemeColor('text'),
|
||||
flexShrink: 1,
|
||||
fontSize: MD_FONT_SIZE,
|
||||
marginLeft: 32,
|
||||
opacity: 0.90
|
||||
},
|
||||
|
||||
/**
|
||||
* Container style for a generic item rendered in the menu.
|
||||
*/
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: MD_ITEM_HEIGHT
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style that is not directly used as a style object.
|
||||
*/
|
||||
underlayColor: ColorPalette.overflowMenuItemUnderlay
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the label in a generic item rendered in the menu.
|
||||
*/
|
||||
labelStyle: {
|
||||
color: schemeColor('text'),
|
||||
flexShrink: 1,
|
||||
fontSize: MD_FONT_SIZE,
|
||||
marginLeft: 32,
|
||||
opacity: 0.90
|
||||
expandIcon: {
|
||||
color: schemeColor('icon'),
|
||||
fontSize: 16,
|
||||
opacity: 0.7
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -159,21 +183,7 @@ ColorSchemeRegistry.register('BottomSheet', {
|
||||
*/
|
||||
sheet: {
|
||||
backgroundColor: schemeColor('background')
|
||||
},
|
||||
|
||||
/**
|
||||
* Container style for a generic item rendered in the menu.
|
||||
*/
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: MD_ITEM_HEIGHT
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style that is not directly used as a style object.
|
||||
*/
|
||||
underlayColor: ColorPalette.overflowMenuItemUnderlay
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
1
react/features/base/icons/svg/drag-handle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><defs><path id="a" d="M0 0h24v24H0V0z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><path clip-path="url(#b)" d="M20 9H4v2h16V9zM4 15h16v-2H4v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 311 B |
@@ -25,6 +25,7 @@ export { default as IconDeviceHeadphone } from './headset.svg';
|
||||
export { default as IconDeviceSpeaker } from './volume.svg';
|
||||
export { default as IconDominantSpeaker } from './dominant-speaker.svg';
|
||||
export { default as IconDownload } from './download.svg';
|
||||
export { default as IconDragHandle } from './drag-handle.svg';
|
||||
export { default as IconEventNote } from './event_note.svg';
|
||||
export { default as IconExitFullScreen } from './exit-full-screen.svg';
|
||||
export { default as IconFeedback } from './feedback.svg';
|
||||
|
||||
@@ -14,6 +14,7 @@ export const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
export const JitsiConnectionEvents = JitsiMeetJS.events.connection;
|
||||
export const JitsiConnectionQualityEvents
|
||||
= JitsiMeetJS.events.connectionQuality;
|
||||
export const JitsiDetectionEvents = JitsiMeetJS.events.detection;
|
||||
export const JitsiE2ePingEvents = JitsiMeetJS.events.e2eping;
|
||||
export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices;
|
||||
export const JitsiParticipantConnectionStatus
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
STORE_VIDEO_TRANSFORM,
|
||||
TOGGLE_CAMERA_FACING_MODE
|
||||
} from './actionTypes';
|
||||
import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
MEDIA_TYPE,
|
||||
VIDEO_MUTISM_AUTHORITY
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Action to adjust the availability of the local audio.
|
||||
@@ -89,6 +93,7 @@ export function setVideoAvailable(available: boolean) {
|
||||
*
|
||||
* @param {boolean} muted - True if the local video is to be muted or false if
|
||||
* the local video is to be unmuted.
|
||||
* @param {MEDIA_TYPE} mediaType - The type of media.
|
||||
* @param {number} authority - The {@link VIDEO_MUTISM_AUTHORITY} which is
|
||||
* muting/unmuting the local video.
|
||||
* @param {boolean} ensureTrack - True if we want to ensure that a new track is
|
||||
@@ -97,6 +102,7 @@ export function setVideoAvailable(available: boolean) {
|
||||
*/
|
||||
export function setVideoMuted(
|
||||
muted: boolean,
|
||||
mediaType: MEDIA_TYPE = MEDIA_TYPE.VIDEO,
|
||||
authority: number = VIDEO_MUTISM_AUTHORITY.USER,
|
||||
ensureTrack: boolean = false) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
@@ -107,6 +113,8 @@ export function setVideoMuted(
|
||||
|
||||
return dispatch({
|
||||
type: SET_VIDEO_MUTED,
|
||||
authority,
|
||||
mediaType,
|
||||
ensureTrack,
|
||||
muted: newValue
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ export const CAMERA_FACING_MODE = {
|
||||
*/
|
||||
export const MEDIA_TYPE = {
|
||||
AUDIO: 'audio',
|
||||
PRESENTER: 'presenter',
|
||||
VIDEO: 'video'
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@ import { getPropertyValue } from '../settings';
|
||||
import { setTrackMuted, TRACK_ADDED } from '../tracks';
|
||||
|
||||
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
|
||||
import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
MEDIA_TYPE,
|
||||
VIDEO_MUTISM_AUTHORITY
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
import {
|
||||
_AUDIO_INITIAL_MEDIA_STATE,
|
||||
@@ -45,7 +49,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
const { track } = action;
|
||||
|
||||
track.local && _syncTrackMutedState(store, track);
|
||||
// Don't sync track mute state with the redux store for screenshare
|
||||
// since video mute state represents local camera mute state only.
|
||||
track.local && track.videoType !== 'desktop'
|
||||
&& _syncTrackMutedState(store, track);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -72,7 +79,7 @@ function _appStateChanged({ dispatch }, next, action) {
|
||||
|
||||
sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
|
||||
|
||||
dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
|
||||
dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
@@ -94,7 +101,11 @@ function _setAudioOnly({ dispatch }, next, action) {
|
||||
|
||||
sendAnalytics(createTrackMutedEvent('video', 'audio-only mode', audioOnly));
|
||||
|
||||
dispatch(setVideoMuted(audioOnly, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, ensureVideoTrack));
|
||||
// Make sure we mute both the desktop and video tracks.
|
||||
dispatch(setVideoMuted(
|
||||
audioOnly, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, ensureVideoTrack));
|
||||
dispatch(setVideoMuted(
|
||||
audioOnly, MEDIA_TYPE.PRESENTER, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, ensureVideoTrack));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
@@ -231,7 +242,9 @@ function _setRoom({ dispatch, getState }, next, action) {
|
||||
*/
|
||||
function _syncTrackMutedState({ getState }, track) {
|
||||
const state = getState()['features/base/media'];
|
||||
const muted = Boolean(state[track.mediaType].muted);
|
||||
const mediaType = track.mediaType === MEDIA_TYPE.PRESENTER
|
||||
? MEDIA_TYPE.VIDEO : track.mediaType;
|
||||
const muted = Boolean(state[mediaType].muted);
|
||||
|
||||
// XXX If muted state of track when it was added is different from our media
|
||||
// muted state, we need to mute track and explicitly modify 'muted' property
|
||||
|
||||
@@ -23,6 +23,11 @@ type Props = {
|
||||
*/
|
||||
avatarSize?: number,
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
avatarStatus?: ?string,
|
||||
|
||||
/**
|
||||
* External style to be applied to the avatar (icon).
|
||||
*/
|
||||
@@ -83,6 +88,7 @@ export default class AvatarListItem extends Component<Props> {
|
||||
const {
|
||||
avatarOnly,
|
||||
avatarSize = AVATAR_SIZE,
|
||||
avatarStatus,
|
||||
avatarStyle
|
||||
} = this.props;
|
||||
const { avatar, colorBase, lines, title } = this.props.item;
|
||||
@@ -96,6 +102,7 @@ export default class AvatarListItem extends Component<Props> {
|
||||
colorBase = { colorBase }
|
||||
displayName = { title }
|
||||
size = { avatarSize }
|
||||
status = { avatarStatus }
|
||||
style = { avatarStyle }
|
||||
url = { avatar } />
|
||||
{ avatarOnly || <Container style = { styles.listItemDetails }>
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* The type of Redux action which sets the noSrcDataNotificationUid state representing the UID of the previous
|
||||
* no data from source notification. Used to check if such a notification was previously displayed.
|
||||
*
|
||||
* {
|
||||
* type: SET_NO_SRC_DATA_NOTIFICATION_UID,
|
||||
* uid: ?number
|
||||
* }
|
||||
*/
|
||||
export const SET_NO_SRC_DATA_NOTIFICATION_UID = 'SET_NO_SRC_DATA_NOTIFICATION_UID';
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched to disable screensharing or to start the
|
||||
* flow for enabling screenshare.
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { getLocalParticipant } from '../participants';
|
||||
|
||||
import {
|
||||
SET_NO_SRC_DATA_NOTIFICATION_UID,
|
||||
TOGGLE_SCREENSHARING,
|
||||
TRACK_ADDED,
|
||||
TRACK_CREATE_CANCELED,
|
||||
@@ -342,6 +343,9 @@ export function trackAdded(track) {
|
||||
let isReceivingData, noDataFromSourceNotificationInfo, participantId;
|
||||
|
||||
if (local) {
|
||||
// Reset the no data from src notification state when we change the track, as it's context is set
|
||||
// on a per device basis.
|
||||
dispatch(setNoSrcDataNotificationUid());
|
||||
const participant = getLocalParticipant(getState);
|
||||
|
||||
if (participant) {
|
||||
@@ -358,6 +362,12 @@ export function trackAdded(track) {
|
||||
});
|
||||
|
||||
dispatch(notificationAction);
|
||||
|
||||
// Set the notification ID so that other parts of the application know that this was
|
||||
// displayed in the context of the current device.
|
||||
// I.E. The no-audio-signal notification shouldn't be displayed if this was already shown.
|
||||
dispatch(setNoSrcDataNotificationUid(notificationAction.uid));
|
||||
|
||||
noDataFromSourceNotificationInfo = { uid: notificationAction.uid };
|
||||
} else {
|
||||
const timeout = setTimeout(() => dispatch(showNoDataFromSourceVideoError(track)), 5000);
|
||||
@@ -638,3 +648,20 @@ function _trackCreateCanceled(mediaType) {
|
||||
trackType: mediaType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets UID of the displayed no data from source notification. Used to track
|
||||
* if the notification was previously displayed in this context.
|
||||
*
|
||||
* @param {number} uid - Notification UID.
|
||||
* @returns {{
|
||||
* type: SET_NO_AUDIO_SIGNAL_UID,
|
||||
* uid: number
|
||||
* }}
|
||||
*/
|
||||
export function setNoSrcDataNotificationUid(uid) {
|
||||
return {
|
||||
type: SET_NO_SRC_DATA_NOTIFICATION_UID,
|
||||
uid
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,47 @@ import {
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Creates a local video track for presenter. The constraints are computed based
|
||||
* on the height of the desktop that is being shared.
|
||||
*
|
||||
* @param {Object} options - The options with which the local presenter track
|
||||
* is to be created.
|
||||
* @param {string|null} [options.cameraDeviceId] - Camera device id or
|
||||
* {@code undefined} to use app's settings.
|
||||
* @param {number} desktopHeight - The height of the desktop that is being
|
||||
* shared.
|
||||
* @returns {Promise<JitsiLocalTrack>}
|
||||
*/
|
||||
export async function createLocalPresenterTrack(options, desktopHeight) {
|
||||
const { cameraDeviceId } = options;
|
||||
|
||||
// compute the constraints of the camera track based on the resolution
|
||||
// of the desktop screen that is being shared.
|
||||
const cameraHeights = [ 180, 270, 360, 540, 720 ];
|
||||
const proportion = 4;
|
||||
const result = cameraHeights.find(
|
||||
height => (desktopHeight / proportion) < height);
|
||||
const constraints = {
|
||||
video: {
|
||||
aspectRatio: 4 / 3,
|
||||
height: {
|
||||
ideal: result
|
||||
}
|
||||
}
|
||||
};
|
||||
const [ videoTrack ] = await JitsiMeetJS.createLocalTracks(
|
||||
{
|
||||
cameraDeviceId,
|
||||
constraints,
|
||||
devices: [ 'video' ]
|
||||
});
|
||||
|
||||
videoTrack.type = MEDIA_TYPE.PRESENTER;
|
||||
|
||||
return videoTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create local tracks of specific types.
|
||||
*
|
||||
@@ -53,11 +94,15 @@ export function createLocalTracksF(
|
||||
|
||||
const state = store.getState();
|
||||
const {
|
||||
constraints,
|
||||
desktopSharingFrameRate,
|
||||
firefox_fake_device, // eslint-disable-line camelcase
|
||||
resolution
|
||||
} = state['features/base/config'];
|
||||
const constraints = options.constraints
|
||||
?? state['features/base/config'].constraints;
|
||||
|
||||
// Do not load blur effect if option for ignoring effects is present.
|
||||
// This is needed when we are creating a video track for presenter mode.
|
||||
const loadEffectsPromise = state['features/blur'].blurEnabled
|
||||
? getBlurEffect()
|
||||
.then(blurEffect => [ blurEffect ])
|
||||
@@ -157,6 +202,18 @@ export function getLocalVideoTrack(tracks) {
|
||||
return getLocalTrack(tracks, MEDIA_TYPE.VIDEO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the media type of the local video, presenter or video.
|
||||
*
|
||||
* @param {Track[]} tracks - List of all tracks.
|
||||
* @returns {MEDIA_TYPE}
|
||||
*/
|
||||
export function getLocalVideoType(tracks) {
|
||||
const presenterTrack = getLocalTrack(tracks, MEDIA_TYPE.PRESENTER);
|
||||
|
||||
return presenterTrack ? MEDIA_TYPE.PRESENTER : MEDIA_TYPE.VIDEO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns track of specified media type for specified participant id.
|
||||
*
|
||||
@@ -197,6 +254,29 @@ export function getTracksByMediaType(tracks, mediaType) {
|
||||
return tracks.filter(t => t.mediaType === mediaType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the local video track in the given set of tracks is muted.
|
||||
*
|
||||
* @param {Track[]} tracks - List of all tracks.
|
||||
* @returns {Track[]}
|
||||
*/
|
||||
export function isLocalVideoTrackMuted(tracks) {
|
||||
const presenterTrack = getLocalTrack(tracks, MEDIA_TYPE.PRESENTER);
|
||||
const videoTrack = getLocalTrack(tracks, MEDIA_TYPE.VIDEO);
|
||||
|
||||
// Make sure we check the mute status of only camera tracks, i.e.,
|
||||
// presenter track when it exists, camera track when the presenter
|
||||
// track doesn't exist.
|
||||
if (presenterTrack) {
|
||||
return isLocalTrackMuted(tracks, MEDIA_TYPE.PRESENTER);
|
||||
} else if (videoTrack) {
|
||||
return videoTrack.videoType === 'camera'
|
||||
? isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO) : true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the first local track in the given tracks set is muted.
|
||||
*
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
SET_AUDIO_MUTED,
|
||||
SET_CAMERA_FACING_MODE,
|
||||
SET_VIDEO_MUTED,
|
||||
VIDEO_MUTISM_AUTHORITY,
|
||||
TOGGLE_CAMERA_FACING_MODE,
|
||||
toggleCameraFacingMode
|
||||
} from '../media';
|
||||
@@ -89,7 +90,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return;
|
||||
}
|
||||
|
||||
_setMuted(store, action, MEDIA_TYPE.VIDEO);
|
||||
_setMuted(store, action, action.mediaType);
|
||||
break;
|
||||
|
||||
case TOGGLE_CAMERA_FACING_MODE: {
|
||||
@@ -131,10 +132,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const { jitsiTrack } = action.track;
|
||||
const muted = jitsiTrack.isMuted();
|
||||
const participantID = jitsiTrack.getParticipantId();
|
||||
const isVideoTrack = jitsiTrack.isVideoTrack();
|
||||
const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO;
|
||||
|
||||
if (isVideoTrack) {
|
||||
if (jitsiTrack.isLocal()) {
|
||||
if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) {
|
||||
APP.conference.mutePresenter(muted);
|
||||
}
|
||||
|
||||
// Make sure we change the video mute state only for camera tracks.
|
||||
if (jitsiTrack.isLocal() && jitsiTrack.videoType !== 'desktop') {
|
||||
APP.conference.setVideoMuteStatus(muted);
|
||||
} else {
|
||||
APP.UI.setVideoMuted(participantID, muted);
|
||||
@@ -255,7 +261,7 @@ function _removeNoDataFromSourceNotification({ getState, dispatch }, track) {
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _setMuted(store, { ensureTrack, muted }, mediaType: MEDIA_TYPE) {
|
||||
function _setMuted(store, { ensureTrack, authority, muted }, mediaType: MEDIA_TYPE) {
|
||||
const localTrack
|
||||
= _getLocalTrack(store, mediaType, /* includePending */ true);
|
||||
|
||||
@@ -265,8 +271,12 @@ function _setMuted(store, { ensureTrack, muted }, mediaType: MEDIA_TYPE) {
|
||||
// `jitsiTrack`, then the `muted` state will be applied once the
|
||||
// `jitsiTrack` is created.
|
||||
const { jitsiTrack } = localTrack;
|
||||
const isAudioOnly = authority === VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY;
|
||||
|
||||
jitsiTrack && setTrackMuted(jitsiTrack, muted);
|
||||
// screenshare cannot be muted or unmuted using the video mute button
|
||||
// anymore, unless it is muted by audioOnly.
|
||||
jitsiTrack && (jitsiTrack.videoType !== 'desktop' || isAudioOnly)
|
||||
&& setTrackMuted(jitsiTrack, muted);
|
||||
} else if (!muted && ensureTrack && typeof APP === 'undefined') {
|
||||
// FIXME: This only runs on mobile now because web has its own way of
|
||||
// creating local tracks. Adjust the check once they are unified.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { PARTICIPANT_ID_CHANGED } from '../participants';
|
||||
import { ReducerRegistry } from '../redux';
|
||||
import { ReducerRegistry, set } from '../redux';
|
||||
|
||||
import {
|
||||
SET_NO_SRC_DATA_NOTIFICATION_UID,
|
||||
TRACK_ADDED,
|
||||
TRACK_CREATE_CANCELED,
|
||||
TRACK_CREATE_ERROR,
|
||||
@@ -133,3 +134,17 @@ ReducerRegistry.register('features/base/tracks', (state = [], action) => {
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Listen for actions that mutate the no-src-data state, like the current notification id
|
||||
*/
|
||||
ReducerRegistry.register('features/base/no-src-data', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case SET_NO_SRC_DATA_NOTIFICATION_UID:
|
||||
return set(state, 'noSrcDataNotificationUid', action.uid);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ const EMOTICON_REGEXP_ARRAY: Array<Array<Object>> = [];
|
||||
// Adding slack-type emoji format
|
||||
escapedValues.push(escapeRegexp(`:${key}:`));
|
||||
|
||||
const regexp = `(${escapedValues.join('|')})`;
|
||||
const regexp = `\\B(${escapedValues.join('|')})\\B`;
|
||||
|
||||
EMOTICON_REGEXP_ARRAY.push([ new RegExp(regexp, 'g'), value ]);
|
||||
}
|
||||
|
||||
@@ -443,6 +443,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
<AvatarListItem
|
||||
avatarOnly = { true }
|
||||
avatarSize = { AVATAR_SIZE }
|
||||
avatarStatus = { item.status }
|
||||
avatarStyle = { styles.avatar }
|
||||
avatarTextStyle = { styles.avatarText }
|
||||
item = { renderableItem }
|
||||
@@ -497,6 +498,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
style = { styles.itemWrapper }>
|
||||
<AvatarListItem
|
||||
avatarSize = { AVATAR_SIZE }
|
||||
avatarStatus = { item.status }
|
||||
avatarStyle = { styles.avatar }
|
||||
avatarTextStyle = { styles.avatarText }
|
||||
item = { renderableItem }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import Avatar from '@atlaskit/avatar';
|
||||
import InlineMessage from '@atlaskit/inline-message';
|
||||
import React from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
|
||||
import { Avatar } from '../../../../base/avatar';
|
||||
import { Dialog, hideDialog } from '../../../../base/dialog';
|
||||
import { translate, translateToHTML } from '../../../../base/i18n';
|
||||
import { Icon, IconPhone } from '../../../../base/icons';
|
||||
@@ -289,13 +289,15 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
return {
|
||||
content: user.name,
|
||||
elemBefore: <Avatar
|
||||
size = 'small'
|
||||
src = { user.avatar } />,
|
||||
className = { 'avatar-small' }
|
||||
status = { user.status }
|
||||
url = { user.avatar } />,
|
||||
item: user,
|
||||
tag: {
|
||||
elemBefore: <Avatar
|
||||
size = 'xsmall'
|
||||
src = { user.avatar } />
|
||||
className = { 'avatar-xsmall' }
|
||||
status = { user.status }
|
||||
url = { user.avatar } />
|
||||
},
|
||||
value: user.id || user.user_id
|
||||
};
|
||||
|
||||
@@ -270,8 +270,8 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||
<View style = { styles.deviceRow } >
|
||||
<Icon
|
||||
src = { icon }
|
||||
style = { [ styles.deviceIcon, _bottomSheetStyles.iconStyle, selectedStyle ] } />
|
||||
<Text style = { [ styles.deviceText, _bottomSheetStyles.labelStyle, selectedStyle ] } >
|
||||
style = { [ styles.deviceIcon, _bottomSheetStyles.buttons.iconStyle, selectedStyle ] } />
|
||||
<Text style = { [ styles.deviceText, _bottomSheetStyles.buttons.labelStyle, selectedStyle ] } >
|
||||
{ text }
|
||||
</Text>
|
||||
</View>
|
||||
@@ -292,8 +292,8 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||
<View style = { styles.deviceRow } >
|
||||
<Icon
|
||||
src = { deviceInfoMap.SPEAKER.icon }
|
||||
style = { [ styles.deviceIcon, _bottomSheetStyles.iconStyle ] } />
|
||||
<Text style = { [ styles.deviceText, _bottomSheetStyles.labelStyle ] } >
|
||||
style = { [ styles.deviceIcon, _bottomSheetStyles.buttons.iconStyle ] } />
|
||||
<Text style = { [ styles.deviceText, _bottomSheetStyles.buttons.labelStyle ] } >
|
||||
{ t('audioDevices.none') }
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { Alert, Platform } from 'react-native';
|
||||
import { Alert, NativeModules, Platform } from 'react-native';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
|
||||
@@ -36,6 +36,7 @@ import CallKit from './CallKit';
|
||||
import ConnectionService from './ConnectionService';
|
||||
import { isCallIntegrationEnabled } from './functions';
|
||||
|
||||
const { AudioMode } = NativeModules;
|
||||
const CallIntegration = CallKit || ConnectionService;
|
||||
|
||||
/**
|
||||
@@ -185,13 +186,25 @@ function _conferenceJoined({ getState }, next, action) {
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
CallIntegration.reportConnectedOutgoingCall(callUUID).then(() => {
|
||||
// iOS 13 doesn't like the mute state to be false before the call is started
|
||||
// so we update it here in case the user selected startWithAudioMuted.
|
||||
if (Platform.OS === 'ios') {
|
||||
_updateCallIntegrationMuted(action.conference, getState());
|
||||
}
|
||||
});
|
||||
CallIntegration.reportConnectedOutgoingCall(callUUID)
|
||||
.then(() => {
|
||||
// iOS 13 doesn't like the mute state to be false before the call is started
|
||||
// so we update it here in case the user selected startWithAudioMuted.
|
||||
if (Platform.OS === 'ios') {
|
||||
_updateCallIntegrationMuted(action.conference, getState());
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Currently this error code is emitted only by Android.
|
||||
//
|
||||
if (error.code === 'CONNECTION_NOT_FOUND_ERROR') {
|
||||
// Some Samsung devices will fail to fully engage ConnectionService if no SIM card
|
||||
// was ever installed on the device. We could check for it, but it would require
|
||||
// the CALL_PHONE permission, which is not something we want to do, so fallback to
|
||||
// not using ConnectionService.
|
||||
_handleConnectionServiceFailure(getState());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -276,7 +289,8 @@ function _conferenceWillJoin({ dispatch, getState }, next, action) {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Currently this error code is emitted only by Android.
|
||||
// Currently this error codes are emitted only by Android.
|
||||
//
|
||||
if (error.code === 'CREATE_OUTGOING_CALL_FAILED') {
|
||||
// We're not tracking the call anymore - it doesn't exist on
|
||||
// the native side.
|
||||
@@ -290,12 +304,45 @@ function _conferenceWillJoin({ dispatch, getState }, next, action) {
|
||||
{ text: 'OK' }
|
||||
],
|
||||
{ cancelable: false });
|
||||
} else if (error.code === 'SECURITY_ERROR') {
|
||||
// Some devices fail because the CALL_PHONE permission is not granted, which is
|
||||
// nonsense, because it's not needed for self-managed connections.
|
||||
|
||||
_handleConnectionServiceFailure(state);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a ConnectionService fatal error by falling back to non-ConnectionService device management.
|
||||
*
|
||||
* @param {Object} state - Redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleConnectionServiceFailure(state: Object) {
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (conference) {
|
||||
// We're not tracking the call anymore.
|
||||
delete conference.callUUID;
|
||||
|
||||
// ConnectionService has fatally failed. Alas, this also means audio device management would be broken, so
|
||||
// fallback to not using ConnectionService.
|
||||
// NOTE: We are not storing this in Settings, in case it's a transient issue, as far fetched as
|
||||
// that may be.
|
||||
if (AudioMode.setUseConnectionService) {
|
||||
AudioMode.setUseConnectionService(false);
|
||||
|
||||
const hasVideo = !isVideoMutedByAudioOnly(state);
|
||||
|
||||
// Set the desired audio mode, since we just reset the whole thing.
|
||||
AudioMode.setMode(hasVideo ? AudioMode.VIDEO_CALL : AudioMode.AUDIO_CALL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles CallKit's event {@code performEndCallAction}.
|
||||
*
|
||||
|
||||
11
react/features/no-audio-signal/actionTypes.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* The type of Redux action which sets the pending notification UID
|
||||
* to use it when hiding the notification is necessary, or unset it when
|
||||
* undefined (or no param) is passed.
|
||||
*
|
||||
* {
|
||||
* type: SET_CURRENT_NOTIFICATION_UID,
|
||||
* uid: ?number
|
||||
* }
|
||||
*/
|
||||
export const SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID = 'SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID';
|
||||
21
react/features/no-audio-signal/actions.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import { SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Sets UID of the the pending notification to use it when hiding
|
||||
* the notification is necessary, or unset it when undefined (or no param) is
|
||||
* passed.
|
||||
*
|
||||
* @param {?number} uid - The UID of the notification.
|
||||
* @returns {{
|
||||
* type: SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID,
|
||||
* uid: number
|
||||
* }}
|
||||
*/
|
||||
export function setNoAudioSignalNotificationUid(uid: ?number) {
|
||||
return {
|
||||
type: SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID,
|
||||
uid
|
||||
};
|
||||
}
|
||||
6
react/features/no-audio-signal/constants.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* The identifier of the sound to be played when we got an event for no audio signal.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const NO_AUDIO_SIGNAL_SOUND_ID = 'NO_AUDIO_SIGNAL_SOUND_ID';
|
||||
4
react/features/no-audio-signal/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// @flow
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
135
react/features/no-audio-signal/middleware.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// @flow
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||
import { CONFERENCE_JOINED } from '../base/conference';
|
||||
import {
|
||||
formatDeviceLabel,
|
||||
setAudioInputDevice
|
||||
} from '../base/devices';
|
||||
import JitsiMeetJS, { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { updateSettings } from '../base/settings';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
import { hideNotification, showNotification } from '../notifications';
|
||||
|
||||
import { setNoAudioSignalNotificationUid } from './actions';
|
||||
import { NO_AUDIO_SIGNAL_SOUND_ID } from './constants';
|
||||
import { NO_AUDIO_SIGNAL_SOUND_FILE } from './sounds';
|
||||
|
||||
MiddlewareRegistry.register(store => next => async action => {
|
||||
const result = next(action);
|
||||
const { dispatch } = store;
|
||||
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
dispatch(registerSound(NO_AUDIO_SIGNAL_SOUND_ID, NO_AUDIO_SIGNAL_SOUND_FILE));
|
||||
break;
|
||||
case APP_WILL_UNMOUNT:
|
||||
dispatch(unregisterSound(NO_AUDIO_SIGNAL_SOUND_ID));
|
||||
break;
|
||||
case CONFERENCE_JOINED:
|
||||
_handleNoAudioSignalNotification(store, action);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the logic of displaying the no audio input detected notification as well as finding a valid device on the
|
||||
* system.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified action is being dispatched.
|
||||
* @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is being dispatched in the specified redux
|
||||
* store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
async function _handleNoAudioSignalNotification({ dispatch, getState }, action) {
|
||||
|
||||
const { conference } = action;
|
||||
let confAudioInputState;
|
||||
|
||||
conference.on(JitsiConferenceEvents.AUDIO_INPUT_STATE_CHANGE, hasAudioInput => {
|
||||
const { noAudioSignalNotificationUid } = getState()['features/no-audio-signal'];
|
||||
|
||||
confAudioInputState = hasAudioInput;
|
||||
|
||||
// In case the notification is displayed but the conference detected audio input signal we hide it.
|
||||
if (noAudioSignalNotificationUid && hasAudioInput) {
|
||||
dispatch(hideNotification(noAudioSignalNotificationUid));
|
||||
dispatch(setNoAudioSignalNotificationUid());
|
||||
}
|
||||
});
|
||||
conference.on(JitsiConferenceEvents.NO_AUDIO_INPUT, async () => {
|
||||
const { noSrcDataNotificationUid } = getState()['features/base/no-src-data'];
|
||||
|
||||
// In case the 'no data detected from source' notification was already shown, we prevent the
|
||||
// no audio signal notification as it's redundant i.e. it's clear that the users microphone is
|
||||
// muted from system settings.
|
||||
if (noSrcDataNotificationUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force the flag to false in case AUDIO_INPUT_STATE_CHANGE is received after the notification is displayed,
|
||||
// possibly preventing the notification from displaying because of an outdated state.
|
||||
confAudioInputState = false;
|
||||
|
||||
|
||||
const activeDevice = await JitsiMeetJS.getActiveAudioDevice();
|
||||
|
||||
if (confAudioInputState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In case there is a previous notification displayed just hide it.
|
||||
const { noAudioSignalNotificationUid } = getState()['features/no-audio-signal'];
|
||||
|
||||
if (noAudioSignalNotificationUid) {
|
||||
dispatch(hideNotification(noAudioSignalNotificationUid));
|
||||
dispatch(setNoAudioSignalNotificationUid());
|
||||
}
|
||||
|
||||
|
||||
let descriptionKey = 'toolbar.noAudioSignalDesc';
|
||||
let customActionNameKey;
|
||||
let customActionHandler;
|
||||
|
||||
// In case the detector picked up a device show a notification with a device suggestion
|
||||
if (activeDevice.deviceLabel !== '') {
|
||||
descriptionKey = 'toolbar.noAudioSignalDescSuggestion';
|
||||
|
||||
// Preferably the label should be passed as an argument paired with a i18next string, however
|
||||
// at the point of the implementation the showNotification function only supports doing that for
|
||||
// the description.
|
||||
// TODO Add support for arguments to showNotification title and customAction strings.
|
||||
customActionNameKey = `Use ${formatDeviceLabel(activeDevice.deviceLabel)}`;
|
||||
customActionHandler = () => {
|
||||
// Select device callback
|
||||
dispatch(
|
||||
updateSettings({
|
||||
userSelectedMicDeviceId: activeDevice.deviceId,
|
||||
userSelectedMicDeviceLabel: activeDevice.deviceLabel
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(setAudioInputDevice(activeDevice.deviceId));
|
||||
};
|
||||
}
|
||||
|
||||
const notification = showNotification({
|
||||
titleKey: 'toolbar.noAudioSignalTitle',
|
||||
descriptionKey,
|
||||
customActionNameKey,
|
||||
customActionHandler
|
||||
});
|
||||
|
||||
dispatch(notification);
|
||||
|
||||
dispatch(playSound(NO_AUDIO_SIGNAL_SOUND_ID));
|
||||
|
||||
// Store the current notification uid so we can check for this state and hide it in case
|
||||
// a new track was added, thus changing the context of the notification
|
||||
dispatch(setNoAudioSignalNotificationUid(notification.uid));
|
||||
});
|
||||
}
|
||||
17
react/features/no-audio-signal/reducer.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
|
||||
import { ReducerRegistry, set } from '../base/redux';
|
||||
|
||||
import { SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Reduces the redux actions of the feature no audio signal
|
||||
*/
|
||||
ReducerRegistry.register('features/no-audio-signal', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case SET_NO_AUDIO_SIGNAL_NOTIFICATION_UID:
|
||||
return set(state, 'noAudioSignalNotificationUid', action.uid);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
6
react/features/no-audio-signal/sounds.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* The file used for the no audio signal sound notification.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const NO_AUDIO_SIGNAL_SOUND_FILE = 'noAudioSignal.mp3';
|
||||
@@ -1,11 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { IconLiveStreaming } from '../../../../base/icons';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import { BetaTag } from '../../../../base/toolbox';
|
||||
|
||||
import AbstractLiveStreamButton, {
|
||||
_mapStateToProps as _abstractMapStateToProps,
|
||||
@@ -37,18 +34,6 @@ type Props = AbstractProps & {
|
||||
class LiveStreamButton extends AbstractLiveStreamButton<Props> {
|
||||
icon = IconLiveStreaming;
|
||||
|
||||
/**
|
||||
* Helper function to be implemented by subclasses, which returns
|
||||
* a React Element to display (a beta tag) at the end of the button.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_getElementAfter() {
|
||||
return <BetaTag />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tooltip that should be displayed when the button is disabled.
|
||||
*
|
||||
|
||||
@@ -80,7 +80,7 @@ class RemoteVideoMenu extends Component<Props> {
|
||||
afterClick: this._onCancel,
|
||||
showLabel: true,
|
||||
participantID: participant.id,
|
||||
styles: this.props._bottomSheetStyles
|
||||
styles: this.props._bottomSheetStyles.buttons
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -25,20 +25,3 @@ export function createRnnoiseProcessorPromise() {
|
||||
throw new Error('Rnnoise module binding createRnnoiseProcessor not found!');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the accepted sample length for the rnnoise library. We might want to expose it with flow libdefs.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getSampleLength() {
|
||||
const ns = getJitsiMeetGlobalNS();
|
||||
|
||||
const rnnoiseSample = ns?.effects?.rnnoise?.RNNOISE_SAMPLE_LENGTH;
|
||||
|
||||
if (!rnnoiseSample) {
|
||||
throw new Error('Please call createRnnoiseProcessorPromise first or wait for promise to resolve!');
|
||||
}
|
||||
|
||||
return rnnoiseSample;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
LOCK_STATE_CHANGED,
|
||||
SET_PASSWORD_FAILED
|
||||
} from '../base/conference';
|
||||
@@ -33,6 +34,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(store, next, action);
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(store, next, action);
|
||||
|
||||
case LOCK_STATE_CHANGED: {
|
||||
// TODO Remove this logic when all components interested in the lock
|
||||
// state change event are moved into react/redux.
|
||||
@@ -67,6 +71,25 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles cleanup of lock prompt state when a conference is joined.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified action is being
|
||||
* dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified action to the specified store.
|
||||
* @param {Action} action - The redux action {@code CONFERENCE_JOINED} which
|
||||
* specifies the details associated with joining the conference.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function _conferenceJoined({ dispatch }, next, action) {
|
||||
dispatch(hideDialog(PasswordRequiredPrompt));
|
||||
dispatch(hideDialog(RoomLockPrompt));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors that occur when a conference fails.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
CLEAR_INTERVAL,
|
||||
INTERVAL_TIMEOUT,
|
||||
SET_INTERVAL,
|
||||
timerWorkerScript
|
||||
} from './TimeWorker';
|
||||
|
||||
/**
|
||||
* Represents a modified MediaStream that adds video as pip on a desktop stream.
|
||||
* <tt>JitsiStreamPresenterEffect</tt> does the processing of the original
|
||||
* desktop stream.
|
||||
*/
|
||||
export default class JitsiStreamPresenterEffect {
|
||||
_canvas: HTMLCanvasElement;
|
||||
_ctx: CanvasRenderingContext2D;
|
||||
_desktopElement: HTMLVideoElement;
|
||||
_desktopStream: MediaStream;
|
||||
_frameRate: number;
|
||||
_onVideoFrameTimer: Function;
|
||||
_onVideoFrameTimerWorker: Function;
|
||||
_renderVideo: Function;
|
||||
_videoFrameTimerWorker: Worker;
|
||||
_videoElement: HTMLVideoElement;
|
||||
isEnabled: Function;
|
||||
startEffect: Function;
|
||||
stopEffect: Function;
|
||||
|
||||
/**
|
||||
* Represents a modified MediaStream that adds a camera track at the
|
||||
* bottom right corner of the desktop track using a HTML canvas.
|
||||
* <tt>JitsiStreamPresenterEffect</tt> does the processing of the original
|
||||
* video stream.
|
||||
*
|
||||
* @param {MediaStream} videoStream - The video stream which is user for
|
||||
* creating the canvas.
|
||||
*/
|
||||
constructor(videoStream: MediaStream) {
|
||||
const videoDiv = document.createElement('div');
|
||||
const firstVideoTrack = videoStream.getVideoTracks()[0];
|
||||
const { height, width, frameRate } = firstVideoTrack.getSettings() ?? firstVideoTrack.getConstraints();
|
||||
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._ctx = this._canvas.getContext('2d');
|
||||
|
||||
if (document.body !== null) {
|
||||
document.body.appendChild(this._canvas);
|
||||
}
|
||||
this._desktopElement = document.createElement('video');
|
||||
this._videoElement = document.createElement('video');
|
||||
videoDiv.appendChild(this._videoElement);
|
||||
videoDiv.appendChild(this._desktopElement);
|
||||
if (document.body !== null) {
|
||||
document.body.appendChild(videoDiv);
|
||||
}
|
||||
|
||||
// Set the video element properties
|
||||
this._frameRate = parseInt(frameRate, 10);
|
||||
this._videoElement.width = parseInt(width, 10);
|
||||
this._videoElement.height = parseInt(height, 10);
|
||||
this._videoElement.autoplay = true;
|
||||
this._videoElement.srcObject = videoStream;
|
||||
|
||||
// set the style attribute of the div to make it invisible
|
||||
videoDiv.style.display = 'none';
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onVideoFrameTimer = this._onVideoFrameTimer.bind(this);
|
||||
this._videoFrameTimerWorker = new Worker(timerWorkerScript);
|
||||
this._videoFrameTimerWorker.onmessage = this._onVideoFrameTimer;
|
||||
}
|
||||
|
||||
/**
|
||||
* EventHandler onmessage for the videoFrameTimerWorker WebWorker.
|
||||
*
|
||||
* @private
|
||||
* @param {EventHandler} response - The onmessage EventHandler parameter.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onVideoFrameTimer(response) {
|
||||
if (response.data.id === INTERVAL_TIMEOUT) {
|
||||
this._renderVideo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop function to render the video frame input and draw presenter effect.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_renderVideo() {
|
||||
// adjust the canvas width/height on every frame incase the window has been resized.
|
||||
const [ track ] = this._desktopStream.getVideoTracks();
|
||||
const { height, width } = track.getSettings() ?? track.getConstraints();
|
||||
|
||||
this._canvas.width = parseInt(width, 10);
|
||||
this._canvas.height = parseInt(height, 10);
|
||||
this._ctx.drawImage(this._desktopElement, 0, 0, this._canvas.width, this._canvas.height);
|
||||
this._ctx.drawImage(this._videoElement, this._canvas.width - this._videoElement.width, this._canvas.height
|
||||
- this._videoElement.height, this._videoElement.width, this._videoElement.height);
|
||||
|
||||
// draw a border around the video element.
|
||||
this._ctx.beginPath();
|
||||
this._ctx.lineWidth = 2;
|
||||
this._ctx.strokeStyle = '#A9A9A9'; // dark grey
|
||||
this._ctx.rect(this._canvas.width - this._videoElement.width, this._canvas.height - this._videoElement.height,
|
||||
this._videoElement.width, this._videoElement.height);
|
||||
this._ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the local track supports this effect.
|
||||
*
|
||||
* @param {JitsiLocalTrack} jitsiLocalTrack - Track to apply effect.
|
||||
* @returns {boolean} - Returns true if this effect can run on the
|
||||
* specified track, false otherwise.
|
||||
*/
|
||||
isEnabled(jitsiLocalTrack: Object) {
|
||||
return jitsiLocalTrack.isVideoTrack() && jitsiLocalTrack.videoType === 'desktop';
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts loop to capture video frame and render presenter effect.
|
||||
*
|
||||
* @param {MediaStream} desktopStream - Stream to be used for processing.
|
||||
* @returns {MediaStream} - The stream with the applied effect.
|
||||
*/
|
||||
startEffect(desktopStream: MediaStream) {
|
||||
const firstVideoTrack = desktopStream.getVideoTracks()[0];
|
||||
const { height, width } = firstVideoTrack.getSettings() ?? firstVideoTrack.getConstraints();
|
||||
|
||||
// set the desktop element properties.
|
||||
this._desktopStream = desktopStream;
|
||||
this._desktopElement.width = parseInt(width, 10);
|
||||
this._desktopElement.height = parseInt(height, 10);
|
||||
this._desktopElement.autoplay = true;
|
||||
this._desktopElement.srcObject = desktopStream;
|
||||
this._canvas.width = parseInt(width, 10);
|
||||
this._canvas.height = parseInt(height, 10);
|
||||
this._videoFrameTimerWorker.postMessage({
|
||||
id: SET_INTERVAL,
|
||||
timeMs: 1000 / this._frameRate
|
||||
});
|
||||
|
||||
return this._canvas.captureStream(this._frameRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the capture and render loop.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
stopEffect() {
|
||||
this._videoFrameTimerWorker.postMessage({
|
||||
id: CLEAR_INTERVAL
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
62
react/features/stream-effects/presenter/TimeWorker.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* SET_INTERVAL constant is used to set interval and it is set in
|
||||
* the id property of the request.data property. timeMs property must
|
||||
* also be set. request.data example:
|
||||
*
|
||||
* {
|
||||
* id: SET_INTERVAL,
|
||||
* timeMs: 33
|
||||
* }
|
||||
*/
|
||||
export const SET_INTERVAL = 1;
|
||||
|
||||
/**
|
||||
* CLEAR_INTERVAL constant is used to clear the interval and it is set in
|
||||
* the id property of the request.data property.
|
||||
*
|
||||
* {
|
||||
* id: CLEAR_INTERVAL
|
||||
* }
|
||||
*/
|
||||
export const CLEAR_INTERVAL = 2;
|
||||
|
||||
/**
|
||||
* INTERVAL_TIMEOUT constant is used as response and it is set in the id
|
||||
* property.
|
||||
*
|
||||
* {
|
||||
* id: INTERVAL_TIMEOUT
|
||||
* }
|
||||
*/
|
||||
export const INTERVAL_TIMEOUT = 3;
|
||||
|
||||
/**
|
||||
* The following code is needed as string to create a URL from a Blob.
|
||||
* The URL is then passed to a WebWorker. Reason for this is to enable
|
||||
* use of setInterval that is not throttled when tab is inactive.
|
||||
*/
|
||||
const code = `
|
||||
var timer;
|
||||
|
||||
onmessage = function(request) {
|
||||
switch (request.data.id) {
|
||||
case ${SET_INTERVAL}: {
|
||||
timer = setInterval(() => {
|
||||
postMessage({ id: ${INTERVAL_TIMEOUT} });
|
||||
}, request.data.timeMs);
|
||||
break;
|
||||
}
|
||||
case ${CLEAR_INTERVAL}: {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
`;
|
||||
|
||||
export const timerWorkerScript
|
||||
= URL.createObjectURL(new Blob([ code ], { type: 'application/javascript' }));
|
||||
19
react/features/stream-effects/presenter/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// @flow
|
||||
|
||||
import JitsiStreamPresenterEffect from './JitsiStreamPresenterEffect';
|
||||
|
||||
/**
|
||||
* Creates a new instance of JitsiStreamPresenterEffect.
|
||||
*
|
||||
* @param {MediaStream} stream - The video stream which will be used for
|
||||
* creating the presenter effect.
|
||||
* @returns {Promise<JitsiStreamPresenterEffect>}
|
||||
*/
|
||||
export function createPresenterEffect(stream: MediaStream) {
|
||||
if (!MediaStreamTrack.prototype.getSettings
|
||||
&& !MediaStreamTrack.prototype.getConstraints) {
|
||||
return Promise.reject(new Error('JitsiStreamPresenterEffect not supported!'));
|
||||
}
|
||||
|
||||
return Promise.resolve(new JitsiStreamPresenterEffect(stream));
|
||||
}
|
||||
@@ -10,6 +10,11 @@ export const RNNOISE_SAMPLE_LENGTH: number = 480;
|
||||
*/
|
||||
const RNNOISE_BUFFER_SIZE: number = RNNOISE_SAMPLE_LENGTH * 4;
|
||||
|
||||
/**
|
||||
* Constant. Rnnoise only takes operates on 44.1Khz float 32 little endian PCM.
|
||||
*/
|
||||
const PCM_FREQUENCY: number = 44100;
|
||||
|
||||
/**
|
||||
* Represents an adaptor for the rnnoise library compiled to webassembly. The class takes care of webassembly
|
||||
* memory management and exposes rnnoise functionality such as PCM audio denoising and VAD (voice activity
|
||||
@@ -131,6 +136,24 @@ export default class RnnoiseProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rnnoise can only operate on a certain PCM array size.
|
||||
*
|
||||
* @returns {number} - The PCM sample array size as required by rnnoise.
|
||||
*/
|
||||
getSampleLength() {
|
||||
return RNNOISE_SAMPLE_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rnnoise can only operate on a certain format of PCM sample namely float 32 44.1Kz.
|
||||
*
|
||||
* @returns {number} - PCM sample frequency as required by rnnoise.
|
||||
*/
|
||||
getRequiredPCMFrequency() {
|
||||
return PCM_FREQUENCY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release any resources required by the rnnoise context this needs to be called
|
||||
* before destroying any context that uses the processor.
|
||||
|
||||
@@ -10,14 +10,13 @@ import {
|
||||
import { setAudioOnly } from '../../base/audio-only';
|
||||
import { translate } from '../../base/i18n';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
VIDEO_MUTISM_AUTHORITY,
|
||||
setVideoMuted
|
||||
} from '../../base/media';
|
||||
import { connect } from '../../base/redux';
|
||||
import { AbstractVideoMuteButton } from '../../base/toolbox';
|
||||
import type { AbstractButtonProps } from '../../base/toolbox';
|
||||
import { isLocalTrackMuted } from '../../base/tracks';
|
||||
import { getLocalVideoType, isLocalVideoTrackMuted } from '../../base/tracks';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
declare var APP: Object;
|
||||
@@ -32,6 +31,11 @@ type Props = AbstractButtonProps & {
|
||||
*/
|
||||
_audioOnly: boolean,
|
||||
|
||||
/**
|
||||
* MEDIA_TYPE of the local video.
|
||||
*/
|
||||
_videoMediaType: string,
|
||||
|
||||
/**
|
||||
* Whether video is currently muted or not.
|
||||
*/
|
||||
@@ -136,10 +140,12 @@ class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
|
||||
this.props.dispatch(
|
||||
setAudioOnly(false, /* ensureTrack */ true));
|
||||
}
|
||||
const mediaType = this.props._videoMediaType;
|
||||
|
||||
this.props.dispatch(
|
||||
setVideoMuted(
|
||||
videoMuted,
|
||||
mediaType,
|
||||
VIDEO_MUTISM_AUTHORITY.USER,
|
||||
/* ensureTrack */ true));
|
||||
|
||||
@@ -167,7 +173,8 @@ function _mapStateToProps(state): Object {
|
||||
|
||||
return {
|
||||
_audioOnly: Boolean(audioOnly),
|
||||
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
|
||||
_videoMediaType: getLocalVideoType(tracks),
|
||||
_videoMuted: isLocalVideoTrackMuted(tracks)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Platform, TouchableOpacity, View } from 'react-native';
|
||||
import Collapsible from 'react-native-collapsible';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
|
||||
import { IconDragHandle } from '../../../base/icons';
|
||||
import { CHAT_ENABLED, IOS_RECORDING_ENABLED, getFeatureFlag } from '../../../base/flags';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
@@ -16,10 +18,12 @@ import { RoomLockButton } from '../../../room-lock';
|
||||
import { ClosedCaptionButton } from '../../../subtitles';
|
||||
import { TileViewButton } from '../../../video-layout';
|
||||
|
||||
import AudioOnlyButton from './AudioOnlyButton';
|
||||
import HelpButton from '../HelpButton';
|
||||
|
||||
import AudioOnlyButton from './AudioOnlyButton';
|
||||
import RaiseHandButton from './RaiseHandButton';
|
||||
import ToggleCameraButton from './ToggleCameraButton';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link OverflowMenu}.
|
||||
@@ -52,6 +56,19 @@ type Props = {
|
||||
dispatch: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* True if the bottom scheet is scrolled to the top.
|
||||
*/
|
||||
scrolledToTop: boolean,
|
||||
|
||||
/**
|
||||
* True if the 'more' button set needas to be rendered.
|
||||
*/
|
||||
showMore: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The exported React {@code Component}. We need it to execute
|
||||
* {@link hideDialog}.
|
||||
@@ -65,7 +82,7 @@ let OverflowMenu_; // eslint-disable-line prefer-const
|
||||
* Implements a React {@code Component} with some extra actions in addition to
|
||||
* those in the toolbar.
|
||||
*/
|
||||
class OverflowMenu extends Component<Props> {
|
||||
class OverflowMenu extends PureComponent<Props, State> {
|
||||
/**
|
||||
* Initializes a new {@code OverflowMenu} instance.
|
||||
*
|
||||
@@ -74,8 +91,16 @@ class OverflowMenu extends Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
scrolledToTop: true,
|
||||
showMore: false
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSwipe = this._onSwipe.bind(this);
|
||||
this._onToggleMenu = this._onToggleMenu.bind(this);
|
||||
this._renderMenuExpandToggle = this._renderMenuExpandToggle.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,37 +110,67 @@ class OverflowMenu extends Component<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _bottomSheetStyles } = this.props;
|
||||
const { showMore } = this.state;
|
||||
|
||||
const buttonProps = {
|
||||
afterClick: this._onCancel,
|
||||
showLabel: true,
|
||||
styles: this.props._bottomSheetStyles
|
||||
styles: _bottomSheetStyles.buttons
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheet onCancel = { this._onCancel }>
|
||||
<BottomSheet
|
||||
onCancel = { this._onCancel }
|
||||
onSwipe = { this._onSwipe }
|
||||
renderHeader = { this._renderMenuExpandToggle }>
|
||||
<AudioRouteButton { ...buttonProps } />
|
||||
<ToggleCameraButton { ...buttonProps } />
|
||||
<AudioOnlyButton { ...buttonProps } />
|
||||
<RoomLockButton { ...buttonProps } />
|
||||
<ClosedCaptionButton { ...buttonProps } />
|
||||
{
|
||||
this.props._recordingEnabled
|
||||
&& <RecordButton { ...buttonProps } />
|
||||
}
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<TileViewButton { ...buttonProps } />
|
||||
<InviteButton { ...buttonProps } />
|
||||
{
|
||||
this.props._chatEnabled
|
||||
&& <InfoDialogButton { ...buttonProps } />
|
||||
}
|
||||
<RaiseHandButton { ...buttonProps } />
|
||||
<SharedDocumentButton { ...buttonProps } />
|
||||
<HelpButton { ...buttonProps } />
|
||||
<Collapsible collapsed = { !showMore }>
|
||||
<RoomLockButton { ...buttonProps } />
|
||||
<ClosedCaptionButton { ...buttonProps } />
|
||||
{
|
||||
this.props._recordingEnabled
|
||||
&& <RecordButton { ...buttonProps } />
|
||||
}
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<TileViewButton { ...buttonProps } />
|
||||
<InviteButton { ...buttonProps } />
|
||||
{
|
||||
this.props._chatEnabled
|
||||
&& <InfoDialogButton { ...buttonProps } />
|
||||
}
|
||||
<RaiseHandButton { ...buttonProps } />
|
||||
<SharedDocumentButton { ...buttonProps } />
|
||||
<HelpButton { ...buttonProps } />
|
||||
</Collapsible>
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
|
||||
_renderMenuExpandToggle: () => React$Element<any>;
|
||||
|
||||
/**
|
||||
* Function to render the menu toggle in the bottom sheet header area.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderMenuExpandToggle() {
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
this.props._bottomSheetStyles.sheet,
|
||||
styles.expandMenuContainer
|
||||
] }>
|
||||
<TouchableOpacity onPress = { this._onToggleMenu }>
|
||||
{ /* $FlowFixMeProps */ }
|
||||
<IconDragHandle style = { this.props._bottomSheetStyles.expandIcon } />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_onCancel: () => boolean;
|
||||
|
||||
/**
|
||||
@@ -133,6 +188,47 @@ class OverflowMenu extends Component<Props> {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_onSwipe: string => void;
|
||||
|
||||
/**
|
||||
* Callback to be invoked when swipe gesture is detected on the menu. Returns true
|
||||
* if the swipe gesture is handled by the menu, false otherwise.
|
||||
*
|
||||
* @param {string} direction - Direction of 'up' or 'down'.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSwipe(direction) {
|
||||
const { showMore } = this.state;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
!showMore && this.setState({
|
||||
showMore: true
|
||||
});
|
||||
|
||||
return !showMore;
|
||||
case 'down':
|
||||
showMore && this.setState({
|
||||
showMore: false
|
||||
});
|
||||
|
||||
return showMore;
|
||||
}
|
||||
}
|
||||
|
||||
_onToggleMenu: () => void;
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the expand menu button is pressed.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleMenu() {
|
||||
this.setState({
|
||||
showMore: !this.state.showMore
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||