Compare commits

..

36 Commits

Author SHA1 Message Date
paweldomas
c2191e3a28 callkit with base/session 2018-08-07 12:20:38 -05:00
paweldomas
72e3e8593d feat(base/session): store 'room' in the session
Stores name of the conference room in the session when it's being
created.
2018-08-07 12:20:38 -05:00
paweldomas
67a8b4915d feat(base/session): add SESSION_CONFIGURED event
The SESSION_CONFIGURED event is fired once the config has been set,
after either being loaded or restored from the storage.
2018-08-07 12:20:38 -05:00
paweldomas
468d4a7150 ref(mobile/external-api): use base/session 2018-08-07 12:20:38 -05:00
paweldomas
2a01e29fec feat: add features/base/session 2018-08-07 12:20:38 -05:00
paweldomas
90a64d30dc ref(base/config): keep 'locationURL' after SET_CONFIG
This is required for the session feature to be able to tell what's
the latest URL the app is working with.
2018-08-07 12:20:38 -05:00
paweldomas
31905d4f63 debug actions 2018-08-07 12:20:38 -05:00
Nik
7e1d97665a fix: only access nested json values when corrent payload type (#3352) 2018-08-07 09:03:31 -07:00
Zoltan Bettenbuk
b978851a0f [RN] Fix streaming on mobile (#3351) 2018-08-06 17:30:32 -07:00
Nik
ef49817eaf fix: add a timer which automatically clears subtitles (#3349) 2018-08-06 14:30:50 -07:00
virtuacoplenny
cac8888b37 feat(welcome-page): be able to open settings dialog (#3327)
* feat(welcome-page): be able to open settings dialog

- Create a getter for getting a settings tab's props so the device
  selection tab can get updated available devices.
- Be able to call a function from a tab after it has mounted. This is
  used for device selection to essentially call enumerateDevices on
  the welcome page so the device selectors are populated.
- Remove event UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED. Instead directly call
  setAudioOutputDeviceId where possible.
- Fix initialization of the audioOutputDeviceId in settings by defaulting
  the audio output device to the one set in settings.

* squash: updateAvailableDevices -> getAvailableDevices, add comment for propsUpdateFunction
2018-08-06 10:24:59 -05:00
Praveen Gupta
81853d971a [WEB] Show final translated speech to text results as subtitles (#3276)
* Shows final translated speech to text results as subtitles

* Use conference from redux state and removes addTranscriptMessage
2018-08-06 11:24:37 +02:00
Lyubo Marinov
b9c5ed3b03 Fixes typo, comment 2018-08-05 17:18:14 -05:00
Lyubo Marinov
0892e0b644 Remove duplication 2018-08-05 17:04:19 -05:00
Bettenbuk Zoltan
b41bf22be7 Replace console with logger 2018-08-05 17:04:19 -05:00
Saúl Ibarra Corretgé
a1cc9bce91 [RN] Drop no longer needed polyfills
They were required only on Android because of its old JSC version. With the JSC
version bump they are no longer required.
2018-08-05 17:04:19 -05:00
Saúl Ibarra Corretgé
8d3cecad86 [Android] Update JSC version
The JSC version used by React Native is about 3 years old, and doesn't implement
things like Symbol or Typed Arrays, which require polyfills. These polyfills are
sometimes a los less performant, as is the case for Typed Arrays.

Bumping an updated JSC version makes both platforms consistent when it comes to
the JavaScript platform.
2018-08-05 17:04:16 -05:00
hristoterezov
bd8559fad6 fix(invite): IFrame api when invalid invitees are passed. 2018-08-03 12:42:38 -05:00
hristoterezov
fb75180632 ref(RecentList): Improvements after review. 2018-08-03 11:25:03 -05:00
Ritwik Heda
046b06e436 added recent list 2018-08-03 11:25:03 -05:00
Дамян Минков
af7c69a1aa Moves google-api in its own feature. (#3339)
* Moves google-api in its own feature.

* Stores the profile email in redux.
2018-08-02 14:56:36 -07:00
Saúl Ibarra Corretgé
7ad0639f7a [RN] Fix setting audio mode for audio-only calls
When a call is tarted in audio only mode due to the switch on the welcome page,
the wrong audio mode was chosen.
2018-08-01 22:12:08 +02:00
paweldomas
54a1853e60 fix(ios/Podfile.lock): bump SDWebImage/Core version 2018-07-31 14:07:17 -05:00
Saúl Ibarra Corretgé
27021ea271 [RN] Replace cached image implementation
Use react-native-fastimage, which uses 2 full-native image impleentations using
well known and mature (native) libraries.

This gets us rid of 2 libraries which were observerd as a source of bugs and
created trouble with dependencies: react-native-fetch-blob and
react-native-img-cache. They are also no longer well maintained.
2018-07-31 14:07:17 -05:00
Saúl Ibarra Corretgé
f5a667ad9e feat(Avatar): simplified code 2018-07-31 14:07:17 -05:00
paweldomas
2b9ce40533 feat(travis): bump image version 2018-07-31 12:54:01 -05:00
paweldomas
d3dd833f21 fix(ios/travis-ci) try pod update
With the fastimage lib Travis complains about:

CocoaPods could not find compatible versions for pod "SDWebImage/Core"
2018-07-31 12:54:01 -05:00
Neil Brown
1cc372868b Update quick-install.md
Tweaks for clarity.
2018-07-31 11:35:18 +02:00
bgrozev
a6956c7c34 Commit from translate.jitsi.org by user bgrozev.: 447 of 447 strings translated (0 fuzzy). 2018-07-30 14:27:03 -05:00
Leonard Kim
aaaa3e05d1 ref(thumbnail): pass in position of remote menu popover 2018-07-30 11:48:52 -05:00
Saúl Ibarra Corretgé
467a5aaae3 ios: run audio mode operations on a dedicated thread
There is no reason for them to run on the main thread, it's safe to call
AVFoundation functions on threads other than the main thread.

The previous code made an incorrect claim about the thread in which the audio
route change notification selector is called: it's called on a secondary thread:
https://developer.apple.com/documentation/avfoundation/avaudiosessionroutechangenotification
2018-07-27 15:39:39 -05:00
Saúl Ibarra Corretgé
243dd16285 android: run all audio and bluetooth operation on a dedicated thread 2018-07-27 15:39:39 -05:00
Saúl Ibarra Corretgé
92001f4d37 android: run WiFi stats operations on a dedicated thread 2018-07-27 15:39:39 -05:00
paweldomas
6a31c59081 ref(media/VideoTrack.native): remove fade animation 2018-07-27 12:08:54 +02:00
paweldomas
11c5b220a1 fix(participants/Avatar.native): disable fade animation
The Image adds a fade effect without asking, so lets explicitly disable
it. More info here:
https://github.com/facebook/react-native/issues/10194
2018-07-27 12:08:54 +02:00
virtuacoplenny
590ad90cd1 ref(video-layout): resize thumbnails first when resizing video area (#3308) 2018-07-26 11:45:04 -07:00
118 changed files with 3314 additions and 1983 deletions

View File

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

View File

@@ -68,3 +68,9 @@
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# FastImage
-keep public class com.dylanvann.fastimage.* {*;}
-keep public class com.dylanvann.fastimage.** {*;}

View File

@@ -18,6 +18,7 @@ allprojects {
repositories {
google()
jcenter()
maven { url "$rootDir/../node_modules/jsc-android/dist" }
// React Native (JS, Obj-C sources, Android binaries) is installed from
// npm.
maven { url "$rootDir/../node_modules/react-native/android" }
@@ -34,6 +35,12 @@ allprojects {
def version = new groovy.json.JsonSlurper().parseText(file.text).version
details.useVersion version
}
if (details.requested.group == 'org.webkit'
&& details.requested.name == 'android-jsc') {
def file = new File("$rootDir/../node_modules/jsc-android/package.json")
def version = new groovy.json.JsonSlurper().parseText(file.text).version
details.useVersion "r${version.tokenize('.')[0]}"
}
}
}
}
@@ -146,7 +153,7 @@ allprojects {
ext {
buildToolsVersion = "26.0.2"
compileSdkVersion = 26
minSdkVersion = 16
minSdkVersion = 21
targetSdkVersion = 26
// The Maven artifact groupdId of the third-party react-native modules which

View File

@@ -25,7 +25,7 @@ dependencies {
compile 'com.facebook.react:react-native:+'
compile project(':react-native-background-timer')
compile project(':react-native-fetch-blob')
compile project(':react-native-fast-image')
compile project(':react-native-immersive')
compile project(':react-native-keep-awake')
compile project(':react-native-linear-gradient')

View File

@@ -25,8 +25,6 @@ import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
@@ -41,6 +39,8 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Module implementing a simple API to select the appropriate audio device for a
@@ -118,10 +118,11 @@ class AudioModeModule
private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
/**
* {@link Handler} for running all operations on the main thread.
* {@link ExecutorService} for running all audio operations on a dedicated
* thread.
*/
private final Handler mainThreadHandler
= new Handler(Looper.getMainLooper());
private static final ExecutorService executor
= Executors.newSingleThreadExecutor();
/**
* {@link Runnable} for running audio device detection the main thread.
@@ -228,7 +229,7 @@ class AudioModeModule
// Do an initial detection on Android >= M.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mainThreadHandler.post(onAudioDeviceChangeRunner);
runInAudioThread(onAudioDeviceChangeRunner);
} else {
// On Android < M, detect if we have an earpiece.
PackageManager pm = reactContext.getPackageManager();
@@ -268,7 +269,7 @@ class AudioModeModule
*/
@ReactMethod
public void getAudioDevices(final Promise promise) {
mainThreadHandler.post(new Runnable() {
runInAudioThread(new Runnable() {
@Override
public void run() {
WritableMap map = Arguments.createMap();
@@ -305,7 +306,7 @@ class AudioModeModule
* Only used on Android >= M.
*/
void onAudioDeviceChange() {
mainThreadHandler.post(onAudioDeviceChangeRunner);
runInAudioThread(onAudioDeviceChangeRunner);
}
/**
@@ -333,7 +334,7 @@ class AudioModeModule
* Only used on Android < M.
*/
void onHeadsetDeviceChange() {
mainThreadHandler.post(new Runnable() {
runInAudioThread(new Runnable() {
@Override
public void run() {
// XXX: isWiredHeadsetOn is not deprecated when used just for
@@ -383,6 +384,14 @@ class AudioModeModule
}
}
/**
* Helper function to run operations on a dedicated thread.
* @param runnable
*/
public void runInAudioThread(Runnable runnable) {
executor.execute(runnable);
}
/**
* Sets the user selected audio device as the active audio device.
*
@@ -390,7 +399,7 @@ class AudioModeModule
*/
@ReactMethod
public void setAudioDevice(final String device) {
mainThreadHandler.post(new Runnable() {
runInAudioThread(new Runnable() {
@Override
public void run() {
if (!availableDevices.contains(device)) {
@@ -461,7 +470,7 @@ class AudioModeModule
}
}
};
mainThreadHandler.post(r);
runInAudioThread(r);
}
/**

View File

@@ -24,8 +24,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
/**
@@ -55,12 +53,6 @@ class BluetoothHeadsetMonitor {
*/
private boolean headsetAvailable = false;
/**
* {@link Handler} for running all operations on the main thread.
*/
private final Handler mainThreadHandler
= new Handler(Looper.getMainLooper());
/**
* Helper for running Bluetooth operations on the main thread.
*/
@@ -200,6 +192,6 @@ class BluetoothHeadsetMonitor {
* {@link AudioModeModule#onAudioDeviceChange()} callback.
*/
private void updateDevices() {
mainThreadHandler.post(updateDevicesRunnable);
audioModeModule.runInAudioThread(updateDevicesRunnable);
}
}

View File

@@ -122,12 +122,12 @@ class ReactInstanceManagerHolder {
.addPackage(new com.BV.LinearGradient.LinearGradientPackage())
.addPackage(new com.calendarevents.CalendarEventsPackage())
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
.addPackage(new com.dylanvann.fastimage.FastImageViewPackage())
.addPackage(new com.facebook.react.shell.MainReactPackage())
.addPackage(new com.i18n.reactnativei18n.ReactNativeI18n())
.addPackage(new com.oblador.vectoricons.VectorIconsPackage())
.addPackage(new com.ocetnik.timer.BackgroundTimerPackage())
.addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
.addPackage(new com.RNFetchBlob.RNFetchBlobPackage())
.addPackage(new com.rnimmersive.RNImmersivePackage())
.addPackage(new com.zmxv.RNSound.RNSoundPackage())
.addPackage(new ReactPackageAdapter() {

View File

@@ -19,8 +19,6 @@ package org.jitsi.meet.sdk;
import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.facebook.react.bridge.Promise;
@@ -36,6 +34,8 @@ import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Module exposing WiFi statistics.
@@ -64,10 +64,10 @@ class WiFiStatsModule
public final static int SIGNAL_LEVEL_SCALE = 101;
/**
* {@link Handler} for running all operations on the main thread.
* {@link ExecutorService} for running all operations on a dedicated thread.
*/
private final Handler mainThreadHandler
= new Handler(Looper.getMainLooper());
private static final ExecutorService executor
= Executors.newSingleThreadExecutor();
/**
* Initializes a new module instance. There shall be a single instance of
@@ -203,6 +203,6 @@ class WiFiStatsModule
}
}
};
mainThreadHandler.post(r);
executor.execute(r);
}
}

View File

@@ -3,8 +3,8 @@ rootProject.name = 'jitsi-meet'
include ':app', ':sdk'
include ':react-native-background-timer'
project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
include ':react-native-fetch-blob'
project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android')
include ':react-native-fast-image'
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
include ':react-native-immersive'
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
include ':react-native-keep-awake'

View File

@@ -35,6 +35,7 @@ import {
conferenceJoined,
conferenceLeft,
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
EMAIL_COMMAND,
lockStateChanged,
@@ -44,6 +45,7 @@ import {
setDesktopSharingEnabled
} from './react/features/base/conference';
import {
getAvailableDevices,
setAudioOutputDeviceId,
updateDeviceList
} from './react/features/base/devices';
@@ -372,6 +374,8 @@ class ConferenceConnector {
case JitsiConferenceErrors.FOCUS_LEFT:
case JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
APP.store.dispatch(conferenceWillLeave(room));
// FIXME the conference should be stopped by the library and not by
// the app. Both the errors above are unrecoverable from the library
// perspective.
@@ -468,6 +472,7 @@ function _connectionFailedHandler(error) {
JitsiConnectionEvents.CONNECTION_FAILED,
_connectionFailedHandler);
if (room) {
APP.store.dispatch(conferenceWillLeave(room));
room.leave();
}
}
@@ -2125,20 +2130,6 @@ export default {
}
);
APP.UI.addListener(
UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
audioOutputDeviceId => {
sendAnalytics(createDeviceChangedEvent('audio', 'output'));
setAudioOutputDeviceId(audioOutputDeviceId, APP.store.dispatch)
.then(() => logger.log('changed audio output device'))
.catch(err => {
logger.warn('Failed to change audio output device. '
+ 'Default or previously set audio output device '
+ 'will be used instead.', err);
});
}
);
APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => {
// FIXME On web video track is stored both in redux and in
@@ -2310,39 +2301,43 @@ export default {
/**
* Inits list of current devices and event listener for device change.
* @private
* @returns {Promise}
*/
_initDeviceList() {
const { mediaDevices } = JitsiMeetJS;
if (mediaDevices.isDeviceListAvailable()
&& mediaDevices.isDeviceChangeAvailable()) {
mediaDevices.enumerateDevices(devices => {
// Ugly way to synchronize real device IDs with local storage
// and settings menu. This is a workaround until
// getConstraints() method will be implemented in browsers.
const { dispatch } = APP.store;
if (this.localAudio) {
dispatch(updateSettings({
micDeviceId: this.localAudio.getDeviceId()
}));
}
if (this.localVideo) {
dispatch(updateSettings({
cameraDeviceId: this.localVideo.getDeviceId()
}));
}
APP.store.dispatch(updateDeviceList(devices));
APP.UI.onAvailableDevicesChanged(devices);
});
this.deviceChangeListener = devices =>
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
mediaDevices.addEventListener(
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
this.deviceChangeListener);
const { dispatch } = APP.store;
return dispatch(getAvailableDevices())
.then(devices => {
// Ugly way to synchronize real device IDs with local
// storage and settings menu. This is a workaround until
// getConstraints() method will be implemented in browsers.
if (this.localAudio) {
dispatch(updateSettings({
micDeviceId: this.localAudio.getDeviceId()
}));
}
if (this.localVideo) {
dispatch(updateSettings({
cameraDeviceId: this.localVideo.getDeviceId()
}));
}
APP.UI.onAvailableDevicesChanged(devices);
});
}
return Promise.resolve();
},
/**
@@ -2493,13 +2488,24 @@ export default {
// before all operations are done.
Promise.all([
requestFeedbackPromise,
room.leave().then(disconnect, disconnect)
this.leaveRoomAndDisconnect()
]).then(values => {
APP.API.notifyReadyToClose();
maybeRedirectToWelcomePage(values[0]);
});
},
/**
* Leaves the room and calls JitsiConnection.disconnect.
*
* @returns {Promise}
*/
leaveRoomAndDisconnect() {
APP.store.dispatch(conferenceWillLeave(room));
return room.leave().then(disconnect, disconnect);
},
/**
* Changes the email for the local user
* @param email {string} the new email

View File

@@ -0,0 +1,43 @@
%navigate-section-list-text {
width: 100%;
font-size: 14px;
line-height: 20px;
color: $welcomePageTitleColor;
text-align: left;
font-family: 'open_sanslight', Helvetica, sans-serif;
}
%navigate-section-list-tile-text {
@extend %navigate-section-list-text;
overflow: hidden;
text-overflow: ellipsis;
float: left;
}
.navigate-section-list-tile {
height: 90px;
width: 260px;
border-radius: 4px;
background-color: #1754A9;
margin-right: 8px;
padding: 16px;
display: inline-block;
box-sizing: border-box;
cursor: pointer;
}
.navigate-section-tile-body {
@extend %navigate-section-list-tile-text;
font-weight: normal;
}
.navigate-section-tile-title {
@extend %navigate-section-list-tile-text;
font-weight: bold;
}
.navigate-section-section-header {
@extend %navigate-section-list-text;
font-weight: bold;
margin-bottom: 16px;
}
.navigate-section-list {
position: relative;
margin-top: 36px;
margin-bottom: 36px;
}

View File

@@ -78,4 +78,5 @@
@import 'modals/invite/add-people';
@import 'deep-linking/main';
@import 'transcription-subtitles';
@import 'navigate_section_list';
/* Modules END */

View File

@@ -4,7 +4,11 @@ This document describes the required steps for a quick Jitsi Meet installation o
Debian Wheezy and other older systems may require additional things to be done. Specifically for Wheezy, [libc needs to be updated](http://lists.jitsi.org/pipermail/users/2015-September/010064.html).
N.B.: All commands are supposed to be run by root. If you are logged in as a regular user with sudo rights, please prepend ___sudo___ to each of the commands.
N.B.:
a.) All commands are supposed to be run by root. If you are logged in as a regular user with sudo rights, please prepend ___sudo___ to each of the commands.
b.) You only need to do this if you want to ___host your own Jitsi server___. If you just want to have a video conference with someone, use https://meet.jit.si instead.
## Basic Jitsi Meet install
@@ -59,6 +63,8 @@ org.ice4j.ice.harvest.NAT_HARVESTER_PUBLIC_ADDRESS=<Public.IP.Address>
See [the documenation of ice4j](https://github.com/jitsi/ice4j/blob/master/doc/configuration.md)
for details.
By default, anyone who has access to your jitsi instance will be able to start a conferencee: if your server is open to the world, anyone can have a chat with anyone else. If you want to limit the ability to start a conference to registered users, set up a "secure domain". Follow the instructions at https://github.com/jitsi/jicofo#secure-domain.
### Open a conference
Launch a web browser (Chrome, Chromium or latest Opera) and enter in the URL bar the hostname (or IP address) you used in the previous step.

View File

@@ -1,26 +1,2 @@
// The type field of react-native application loader's React Element is created
// as number and not Symbol, because it's not been defined by the polyfill yet.
// We import the application renderer, before Symbol is defined, in order to use
// number types as well. Otherwise this will result in the invariant exception,
// because fiber thingy will not recognise root react-native component as React
// Element, but as an Object.
//
// See node_modules/react-native/Libraries/polyfills/babelHelpers.js
// :babelHelpers.createRawReactElement - that's where first react-native element
// is created (super early - it's the app loader).
//
// See node_modules/react-native/Libraries/Renderer/ReactNativeFiber-dev.js
// and look for REACT_ELEMENT_TYPE definition - it's defined later when Symbol
// has been defined and type will not match.
//
// As an alternative solution we could stop using/polyfilling Symbols and
// replace with classpath string constants or some kind of a wrapper around
// that.
import 'react-native/Libraries/ReactNative/renderApplication';
// Android doesn't provide Symbol
import 'es6-symbol/implement';
import './react/index.native';

View File

@@ -163,7 +163,14 @@ var interfaceConfig = {
*
* @type {boolean}
*/
VIDEO_QUALITY_LABEL_DISABLED: false
VIDEO_QUALITY_LABEL_DISABLED: false,
/**
* If true, will display recent list
*
* @type {boolean}
*/
RECENT_LIST_ENABLED: true
/**
* Specify custom URL for downloading android mobile app.

View File

@@ -28,8 +28,8 @@ target 'JitsiMeet' do
pod 'react-native-background-timer',
:path => '../node_modules/react-native-background-timer'
pod 'react-native-fetch-blob',
:path => '../node_modules/react-native-fetch-blob'
pod 'react-native-fast-image',
:path => '../node_modules/react-native-fast-image'
pod 'react-native-keep-awake',
:path => '../node_modules/react-native-keep-awake'
pod 'react-native-locale-detector',

View File

@@ -1,6 +1,7 @@
PODS:
- boost-for-react-native (1.63.0)
- DoubleConversion (1.1.5)
- FLAnimatedImage (1.0.12)
- Folly (2016.09.26.00):
- boost-for-react-native
- DoubleConversion
@@ -12,8 +13,11 @@ PODS:
- React
- react-native-calendar-events (1.6.0):
- React
- react-native-fetch-blob (0.10.6):
- React/Core
- react-native-fast-image (4.0.14):
- FLAnimatedImage
- React
- SDWebImage/Core
- SDWebImage/GIF
- react-native-keep-awake (2.0.6):
- React
- react-native-locale-detector (1.0.0):
@@ -68,6 +72,10 @@ PODS:
- React/Core
- RNVectorIcons (4.4.2):
- React
- SDWebImage/Core (4.4.2)
- SDWebImage/GIF (4.4.2):
- FLAnimatedImage (~> 1.0)
- SDWebImage/Core
- yoga (0.55.4.React)
DEPENDENCIES:
@@ -76,7 +84,7 @@ DEPENDENCIES:
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
- react-native-calendar-events (from `../node_modules/react-native-calendar-events`)
- react-native-fetch-blob (from `../node_modules/react-native-fetch-blob`)
- react-native-fast-image (from `../node_modules/react-native-fast-image`)
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
- react-native-locale-detector (from `../node_modules/react-native-locale-detector`)
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
@@ -98,6 +106,8 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/cocoapods/specs.git:
- boost-for-react-native
- FLAnimatedImage
- SDWebImage
EXTERNAL SOURCES:
DoubleConversion:
@@ -112,8 +122,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-background-timer"
react-native-calendar-events:
:path: "../node_modules/react-native-calendar-events"
react-native-fetch-blob:
:path: "../node_modules/react-native-fetch-blob"
react-native-fast-image:
:path: "../node_modules/react-native-fast-image"
react-native-keep-awake:
:path: "../node_modules/react-native-keep-awake"
react-native-locale-detector:
@@ -132,20 +142,22 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: e22e0762848812a87afd67ffda3998d9ef29170c
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Folly: 211775e49d8da0ca658aebc8eab89d642935755c
glog: 1de0bb937dccdc981596d3b5825ebfb765017ded
React: aa2040dbb6f317b95314968021bd2888816e03d5
react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222
react-native-fast-image: cba3d9bf9c2cf8ddb643d887a686c53a5dd90a2c
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d
ReactNativePermissions: 9f2d9c45c98800795e6c2ed330e25d11a66a8169
RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681
yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a
PODFILE CHECKSUM: e24d0131e937934fbe4d1f0b7ad5947ee0192f58
PODFILE CHECKSUM: 1d5c8382f73d9540fac68d93b32e1d3b58d069ee
COCOAPODS: 1.5.3

View File

@@ -20,6 +20,9 @@
#import <React/RCTLog.h>
@interface AudioMode : NSObject<RCTBridgeModule>
@property(nonatomic, strong) dispatch_queue_t workerQueue;
@end
@implementation AudioMode {
@@ -52,15 +55,18 @@ typedef enum {
if (self) {
_category = nil;
_mode = nil;
dispatch_queue_attr_t attributes =
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL,
QOS_CLASS_USER_INITIATED, -1);
_workerQueue = dispatch_queue_create("WebRTCModule.queue", attributes);
}
return self;
}
- (dispatch_queue_t)methodQueue {
// Make sure all our methods run in the main thread. The route change
// notification runs there so this will make sure it will only be fired
// after our changes have been applied (when we cause them, that is).
return dispatch_get_main_queue();
// Use a dedicated queue for audio mode operations.
return _workerQueue;
}
- (void)routeChanged:(NSNotification*)notification {
@@ -70,12 +76,15 @@ typedef enum {
integerValue];
switch (reason) {
case AVAudioSessionRouteChangeReasonCategoryChange:
case AVAudioSessionRouteChangeReasonCategoryChange: {
// The category has changed. Check if it's the one we want and adjust as
// needed.
[self setCategory:_category mode:_mode error:nil];
// needed. This notification is posted on a secondary thread, so make
// sure we switch to our worker thread.
dispatch_async(_workerQueue, ^{
[self setCategory:_category mode:_mode error:nil];
});
break;
}
default:
// Do nothing.
break;

View File

@@ -130,6 +130,7 @@ cp "${CERT_DIR}/dev-profile.mobileprovision" ~/Library/MobileDevice/Provisionin
npm install
cd ios
pod update
pod install
cd ..

View File

@@ -1,24 +1,19 @@
{
"contactlist": "",
"contactlist_plural": "",
"passwordSetRemotely": "Configurado por outro membro",
"connectionsettings": "Configurações de conexão",
"contactlist_plural": "__count__ Membros",
"passwordSetRemotely": "configurado por outro membro",
"poweredby": "distribuído por",
"feedback": {
"average": "Média",
"bad": "Ruim",
"good": "Boa",
"rateExperience": "Por favor, avalie sua experiência na reunião.",
"veryBad": "Muito ruim",
"veryGood": "Muito boa"
},
"inviteUrlDefaultMsg": "Sua conferência está sendo criada...",
"me": "eu",
"speaker": "Orador",
"raisedHand": "Gostaria de falar",
"defaultNickname": "ex. João Pedro",
"defaultLink": "ex.: __url__",
"callingName": "__name__",
"audioDevices": {
"bluetooth": "Bluetooth",
"headphones": "Fones de ouvido",
"phone": "Celular",
"speaker": "Orador"
},
"audioOnly": {
"audioOnly": "Somente áudio",
"featureToggleDisabled": "A alternância de __feature__ é desativada enquanto estiver no modo somente de áudio"
@@ -27,6 +22,7 @@
"react-nativeGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
"chromeGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
"androidGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
"electronGrantPermissions": "Dê as permissões para usar sua câmera e microfone",
"firefoxGrantPermissions": "Selecione <b><i>Compartilhar Dispositivos Selecionados</i></b> quando seu navegador perguntar pelas permissões.",
"operaGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
"iexplorerGrantPermissions": "Selecione <b><i>OK</i></b> quando seu navegador perguntar pelas permissões.",
@@ -50,46 +46,20 @@
"showSpeakerStats": "Exibir estatísticas do alto falante"
},
"welcomepage": {
"disable": "Não exibir esta página novamente",
"feature1": {
"content": "Não precisa baixar nada. __app__ funciona diretamente no seu navegador. Basta compartilhar a URL da sua conferência com outros para começar.",
"title": "Simples de usar"
},
"feature2": {
"content": "Conferências de vídeo de multipartes funcionam a partir de 128 kbps. Compartilhamento de tela e conferências apenas com áudio são possíveis com muito menos.",
"title": "Largura de banda baixa"
},
"feature3": {
"content": "__app__ é licenciado sob a Licença Apache. Você é livre para baixar, usar, modificar e compartilhar ela com a mesma licença.",
"title": "Código aberto"
},
"feature4": {
"content": "Não há restrições artificiais sobre o número de usuários ou membros da conferência. O poder do servidor e a largura de banda são os únicos fatores limitantes.",
"title": "Usuários ilimitados"
},
"feature5": {
"content": "É fácil compartilhar sua tela com outros. __app__ é ideal para apresentações online, leituras, e sessões de suporte técnico.",
"title": "Compartilhamento de tela"
},
"feature6": {
"content": "Precisa alguma privacidade? Salas de conferência do __app__ podem ser seguras com uma senha para excluir visitantes indesejados e prevenir interrupções.",
"title": "Salas seguras"
},
"feature7": {
"content": "__app_ disponibiliza o Etherpad, um editor de texto colaborativo em tempo real, que é ótimo para reuniões rápidas, escrevendo artigos, e mais.",
"title": "Notas compartilhadas"
},
"feature8": {
"content": "Aprenda sobre seus usuários através de integração fácil com o Piwik, Google Analytics, e outros sistemas de monitoramento e estatísticas.",
"title": "Estatísticas de uso"
"appDescription": "Vá em frente, converse por vídeo com toda a equipe. De fato, convide todos que você conhece. __app__ é uma solução de videoconferência totalmente criptografada e 100% de código aberto que você pode usar todos os dias, a cada dia, gratuitamente — sem necessidade de conta.",
"audioVideoSwitch": {
"audio": "Voz",
"video": "Vídeo"
},
"calendar": "Calendário",
"go": "IR",
"join": "Entrar",
"privacy": "Política de Privacidade",
"roomname": "Digite o nome da sala",
"roomnamePlaceHolder": "Nome da sala",
"roomnameHint": "Digite o nome ou a URL da sala que você deseja entrar. Você pode digitar um nome, e apenas deixe para as pessoas que você quer se reunir digitem o mesmo nome.",
"sendFeedback": "Enviar comentários",
"terms": "Termos"
"terms": "Termos",
"title": "Vídeo conferência mais segura, mais flexível e completamente livre "
},
"startupoverlay": {
"policyText": " ",
@@ -103,22 +73,28 @@
"toolbar": {
"addPeople": "Adicionar pessoas à sua chamada",
"audioonly": "Ativar / desativar modo somente áudio (economiza banda)",
"callQuality": "Gerenciar qualidade da chamada",
"enterFullScreen": "Ver em tela cheia",
"exitFullScreen": "Sair da tela cheia",
"feedback": "Deixar feedback",
"moreActions": "Mais ações",
"mute": "Mudo / Não mudo",
"videomute": "Iniciar ou parar a câmera",
"authenticate": "Autenticar",
"lock": "Travar ou destravar a sala",
"invite": "Compartilhar o link",
"chat": "Abrir ou fechar o bate-papo",
"etherpad": "Abrir ou fechar o documento compartilhado",
"documentOpen": "Abrir documento compartilhado",
"documentClose": "Fechar documento compartilhado",
"sharedvideo": "Compartilhar um vídeo do YouTube",
"sharescreen": "Iniciar ou parar o compartilhamento de tela",
"sharescreen": "Compartilhamento de tela",
"stopSharedVideo": "Parar vídeo do YouTube",
"fullscreen": "Entrar ou sair da tela cheia",
"sip": "Chamar número SIP",
"Settings": "Configurações",
"hangup": "Sair",
"login": "Iniciar sessão",
"logout": "Encerrar sessão",
"dialpad": "Abrir ou fechar teclado de discagem",
"sharedVideoMutedPopup": "Seu vídeo compartilhado foi silenciado para que você possa conversar com os outros membros.",
"micMutedPopup": "Seu microfone foi silenciado para que você aproveite plenamente seu vídeo compartilhado.",
"talkWhileMutedPopup": "Tentando falar? Você está em mudo.",
@@ -127,19 +103,9 @@
"micDisabled": "O microfone não está disponível",
"filmstrip": "Mostrar / ocultar vídeos",
"profile": "Editar seu perfil",
"raiseHand": "Erguer o baixar sua mão"
},
"unsupportedBrowser": {
"appInstalled": "ou se você já tenha isso<br /> <strong>então</strong>",
"appNotInstalled": "Você precisa do <strong>__app__</strong> para começar uma conversa no seu celular",
"downloadApp": "Baixe o Aplicativo",
"joinConversation": "Entrar na conversa",
"startConference": "Comece uma conferência"
},
"bottomtoolbar": {
"chat": "Abrir / fechar bate-papo",
"filmstrip": "Mostrar / ocultar vídeos",
"contactlist": "Veja e convide membros"
"raiseHand": "Erguer o baixar sua mão",
"shortcuts": "Ver atalhos",
"speakerStats": "Estatísticas do Apresentador"
},
"chat": {
"nickname": {
@@ -174,7 +140,7 @@
"moderator": "Moderador",
"videomute": "O membro parou a câmera",
"mute": "O membro está em silêncio",
"kick": "Chutar fora",
"kick": "Expulsar",
"muted": "Mudo",
"domute": "Mudo",
"flip": "Inverter",
@@ -199,7 +165,7 @@
"remoteaddress_plural": "Endereços remotos:",
"transport": "Transporte:",
"bandwidth": "Largura de banda estimada:",
"na": "Volte aqui para informações de conexão uma vez que a conferência inicie",
"na": "Volte aqui para informações de conexão após iniciar a conferência",
"turn": " (virar)",
"quality": {
"good": "Boa",
@@ -213,7 +179,9 @@
"notify": {
"disconnected": "desconectado",
"moderator": "Direitos de moderador concedidos!",
"connected": "conectado",
"connectedOneMember": "__name__ conectado",
"connectedTwoMembers": "__first__ e __second__ conectados",
"connectedThreePlusMembers": "__name__ e __count__ outros conectados",
"somebody": "Alguém",
"me": "Eu",
"focus": "Foco da conferência",
@@ -222,42 +190,42 @@
"grantedToUnknown": "Direitos de moderador concedido para $t(notify.somebody)!",
"muted": "Você iniciou uma conversa em mudo.",
"mutedTitle": "Você está mudo!",
"raisedHand": "Gostaria de falar."
"raisedHand": "Gostaria de falar.",
"suboptimalExperienceTitle": "Alerta do navegador",
"suboptimalExperienceDescription": "Eer ... temos medo de que sua experiência com o __appName__ não seja tão boa aqui. Estamos procurando maneiras de melhorar isso, mas até lá tente usar um dos <a href='static/recommendedBrowsers.html' target='_blank'> navegadores totalmente compatíveis</a>."
},
"dialog": {
"add": "Adicionar",
"allow": "Permitir",
"kickMessage": "Ouch! Você o chutou para fora da reunião!",
"popupErrorTitle": "",
"popupError": "",
"kickMessage": "Ouch! Você foi expulso da reunião!",
"popupErrorTitle": "Popup bloqueado",
"popupError": "Seu navegador está bloqueando janelas popup deste site. Habilite os popups nas configurações de segurança no seu navegador e tente novamente.",
"passwordErrorTitle": "Erro na senha",
"passwordError": "Esta conversa está protegida atualmente por uma senha. Somente o dono da conferência pode definir a senha.",
"passwordError2": "Esta conversa não está protegida por senha atualmente. Somente o dono da conferência pode definir a senha.",
"connectError": "Oops! Alguma coisa está errada e nós não pudemos conectar à conferência.",
"connectErrorWithMsg": "Oops! Alguma coisa está errada e não podemos conectar à conferência: __msg__",
"incorrectPassword": "",
"incorrectPassword": "Usuário ou senha incorretos",
"connecting": "Conectando",
"copy": "Copiar",
"contactSupport": "",
"contactSupport": "Contate o suporte",
"error": "Erro",
"createPassword": "Criar uma senha",
"detectext": "Erro enquanto tenta detectar a extensão de compartilhamento de tela.",
"detectext": "Erro ao detectar a extensão de compartilhamento de tela.",
"failedpermissions": "Falha ao obter permissões para usar o microfone e/ou câmera local.",
"conferenceReloadTitle": "Infelizmente, algo deu errado.",
"conferenceReloadMsg": "Estamos tentando consertar isto. Reconectando em __seconds__ segundos...",
"conferenceDisconnectTitle": "Você foi desconectado.",
"conferenceDisconnectMsg": "Você pode querer verificar sua conexão de rede. Reconectando em __seconds__ segundos ...",
"dismiss": "",
"rejoinNow": "Voltar agora",
"maxUsersLimitReachedTitle": "",
"maxUsersLimitReached": "",
"dismiss": "Dispensar",
"rejoinNow": "Reconectar agora",
"maxUsersLimitReachedTitle": "Limite máximo de membros alcançado",
"maxUsersLimitReached": "O limite para o máximo do número de membros foi alcançado. A conferência está cheia. Contate o dono da reunião ou tente de novo depois!",
"lockTitle": "Bloqueio falhou",
"lockMessage": "Falha ao travar a conferência.",
"warning": "Atenção",
"passwordNotSupportedTitle": "",
"passwordNotSupported": "",
"passwordNotSupportedTitle": "Senha não suportada",
"passwordNotSupported": "Configuração de senha para a reunião não é suportada.",
"internalErrorTitle": "Erro interno",
"internalError": "",
"internalError": "Oops! Alguma coisa está errada. O seguinte erro ocorreu: __error__",
"unableToSwitch": "Impossível trocar o fluxo de vídeo.",
"SLDFailure": "Oops! Alguma coisa está errada e nós falhamos em silenciar! (Falha do SLD)",
"SRDFailure": "Oops! Alguma coisa está errada e nós falhamos em parar o vídeo! (Falha do SRD)",
@@ -274,8 +242,8 @@
"shareVideoLinkError": "Por favor, forneça um link do youtube correto.",
"removeSharedVideoTitle": "Remover vídeo compartilhado",
"removeSharedVideoMsg": "Deseja remover seu vídeo compartilhado?",
"alreadySharedVideoMsg": "",
"alreadySharedVideoTitle": "",
"alreadySharedVideoMsg": "Outro membro já está compartilhando um vídeo. Esta conferência permite apenas um vídeo compartilhado por vez.",
"alreadySharedVideoTitle": "Somente um vídeo compartilhado é permitido por vez",
"WaitingForHost": "Esperando o hospedeiro...",
"WaitForHostMsg": "A conferência <b>__room__</b> não foi iniciada. Se você é o hospedeiro, então autentique-se. Caso contrário, aguarde o hospedeiro chegar.",
"IamHost": "Eu sou o hospedeiro",
@@ -284,20 +252,16 @@
"retry": "Tentar novamente",
"logoutTitle": "Encerrar sessão",
"logoutQuestion": "Deseja encerrar a sessão e finalizar a conferência?",
"sessTerminated": "",
"sessTerminated": "Chamada terminada",
"hungUp": "Você desconectou",
"joinAgain": "Entrar novamente",
"Share": "Compartilhar",
"Save": "Salvar",
"recording": "Gravando",
"recordingToken": "Digite o token de gravação",
"passwordCheck": "Deseja remover a senha?",
"passwordMsg": "Definir uma senha para trancar sua sala",
"shareLink": "Compartilhar o link para a chamada",
"yourPassword": "Digite a nova senha",
"Back": "Voltar",
"serviceUnavailable": "Serviço indisponível",
"gracefulShutdown": "Nosso serviço está desligado para manutenção. Por favor, tente mais tarde.",
"gracefulShutdown": "O sistema está em manutenção. Por favor tente novamente mais tarde.",
"Yes": "Sim",
"reservationError": "Erro de sistema de reserva",
"reservationErrorMsg": "Código do erro: __code__, mensagem: __msg__",
@@ -306,42 +270,40 @@
"token": "token",
"tokenAuthFailedTitle": "Falha de autenticação",
"tokenAuthFailed": "Desculpe, você não está autorizado a entrar nesta chamada.",
"displayNameRequired": "Mostrar o nome é requerido",
"displayNameRequired": "Nome de exibição requerido",
"enterDisplayName": "Digite seu nome de exibição",
"extensionRequired": "Extensão requerida:",
"firefoxExtensionPrompt": "Você precisa instalar uma extensão do Firefox para compartilhar a tela. Tente novamente depois que você <a href='__url__'>pegá-lo aqui</a>!",
"feedbackHelp": "Seu retorno nos ajudará a melhorar nossa experiência de vídeo.",
"feedbackQuestion": "Nos conte sobre sua chamada!",
"thankYou": "Obrigado por usar o __appName__!",
"sorryFeedback": "Lamentamos escutar isso. Gostaria de nos contar mais?",
"sorryFeedback": "Sentimos muito pelos transtornos. Poderia nos das mais detalhes?",
"liveStreaming": "Transmissão ao Vivo",
"streamKey": "Nome/chave do fluxo",
"startLiveStreaming": "Iniciar transmissão ao vivo",
"streamKey": "Chave para transmissão ao vivo",
"startLiveStreaming": "Ir ao vivo agora",
"startRecording": "Parar gravação",
"stopStreamingWarning": "Tem certeza que deseja parar a transmissão ao vivo?",
"stopRecordingWarning": "Tem certeza que deseja parar a gravação?",
"stopLiveStreaming": "Parar a transmissão ao vivo",
"stopRecording": "Parar a gravação",
"doNotShowWarningAgain": "Não exibir este aviso novamente",
"doNotShowMessageAgain": "Não mostre esta mensagem novamente",
"permissionDenied": "Permissão Negada",
"screenSharingFailedToInstall": "",
"screenSharingFailedToInstallTitle": "",
"screenSharingPermissionDeniedError": "",
"micErrorPresent": "Ocorreu um erro conectando seu microfone.",
"cameraErrorPresent": "Ocorreu um erro conectando sua câmera.",
"screenSharingFailedToInstall": "Oops! Falhou a instalação da extensão de compartilhamento de tela.",
"screenSharingFailedToInstallTitle": "A extensão de compartilhamento de tela falhou ao instalar",
"screenSharingFirefoxPermissionDeniedError": "Algo deu errado enquanto estávamos tentando compartilhar sua tela. Por favor, certifique-se de que você nos deu permissão para fazê-lo. ",
"screenSharingFirefoxPermissionDeniedTitle": "Opa! Não foi possível iniciar o compartilhamento de tela.",
"screenSharingPermissionDeniedError": "Oops! Alguma coisa está errada com suas permissões de compartilhamento de tela. Recarregue e tente de novo.",
"cameraUnsupportedResolutionError": "Sua câmera não suporta a resolução de vídeo requerida.",
"cameraUnknownError": "Não pode usar a câmera por uma razão desconhecida.",
"cameraPermissionDeniedError": "Você não tem permissão para usar sua câmera. Você ainda pode entrar na conferência, mas os outros não verão você. Use o botão da câmera na barra de endereço para fixar isto.",
"cameraPermissionDeniedError": "Não foi permitido acessar a sua câmera. Você ainda pode entrar na conferência, mas sem exibir o seu vídeo. Clique no botão da câmera para tentar reparar.",
"cameraNotFoundError": "A câmera não foi encontrada.",
"cameraConstraintFailedError": "Sua câmera não satisfaz algumas condições necessárias.",
"micUnknownError": "Não pode usar o microfone por uma razão desconhecida.",
"micPermissionDeniedError": "Você não tem permissão para usar seu microfone. Você ainda pode entrar na conferência, mas os outros não ouvirão você. Use o botão da câmera na barra de endereço para fixar isto.",
"micPermissionDeniedError": "Não foi permitido acessar o seu microfone. Você ainda pode entrar na conferência, mas sem enviar áudio. Clique no botão do microfone para tentar reparar.",
"micNotFoundError": "O microfone não foi encontrado.",
"micConstraintFailedError": "Seu microfone não satisfaz algumas condições necessárias.",
"micNotSendingDataTitle": "",
"micNotSendingData": "",
"cameraNotSendingDataTitle": "",
"cameraNotSendingData": "",
"micNotSendingDataTitle": "Incapaz de acessar o microfone",
"micNotSendingData": "Estamos incapazes de acessar seu microfone. Selecione outro dispositivo do menu de configurações ou tente recarregar a aplicação.",
"cameraNotSendingDataTitle": "Incapaz de acessar a câmera",
"cameraNotSendingData": "Estamos incapazes de acessar sua câmera. Verifique se outra aplicação está usando este dispositivo, selecione outro dispositivo do menu de configurações ou recarregue a aplicação.",
"goToStore": "Vá para a loja virtual",
"externalInstallationTitle": "Extensão requerida",
"externalInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela.",
@@ -351,7 +313,7 @@
"muteParticipantBody": "Você não está habilitado para tirar o mudo deles, mas eles podem tirar o mudo deles mesmos a qualquer tempo.",
"muteParticipantButton": "Mudo",
"remoteControlTitle": "Conexão de área de trabalho remota",
"remoteControlRequestMessage": "Permitirá __user__ controlar remotamente sua área de trabalho?",
"remoteControlRequestMessage": "Deseja permitir que __user__ controle remotamente sua área de trabalho?",
"remoteControlShareScreenWarning": "Note que se você pressionar \"Permitir\" você vai compartilhar sua tela!",
"remoteControlDeniedMessage": "__user__ rejeitou sua requisição de controle remoto!",
"remoteControlAllowedMessage": "__user__ aceitou sua requisição de controle remoto!",
@@ -404,30 +366,50 @@
"ATTACHED": "Anexado"
},
"recording": {
"busy": "",
"busyTitle": "",
"busy": "Estamos trabalhando para liberar recursos de gravação. Tente novamente em alguns minutos.",
"busyTitle": "Todas as gravações estão atualmente ocupadas",
"buttonTooltip": "Iniciar / parar gravação",
"error": "A gravação falhou. Tente novamente.",
"failedToStart": "Falha ao iniciar a gravação",
"off": "Gravação parada",
"on": "Gravando",
"pending": "Aguardando um participante para iniciar a gravação...",
"unavailable": "",
"unavailableTitle": ""
"serviceName": "Serviço de gravação",
"unavailable": "Oops! O __serviceName__ está indisponível. Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
"unavailableTitle": "Gravação indisponível"
},
"liveStreaming": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"error": "",
"failedToStart": "",
"off": "",
"busy": "Estamos trabalhando para liberar os recursos de transmissão. Tente novamente em alguns minutos.",
"busyTitle": "Todas as transmissões estão atualmente ocupadas",
"buttonTooltip": "Iniciar / Parar transmissão ao vivo",
"changeSignIn": "Alternar contas.",
"choose": "Escolha uma transmissão ao vivo",
"chooseCTA": "Escolha uma opção de transmissão. Você está conectado atualmente como __email__.",
"enterStreamKey": "Insira sua chave de transmissão ao vivo do YouTube aqui.",
"error": "Falha na transmissão ao vivo. Tente de novo.",
"errorAPI": "Ocorreu um erro ao acessar suas transmissões do YouTube. Por favor tente logar novamente.",
"failedToStart": "Falha ao iniciar a transmissão ao vivo",
"off": "Transmissão ao vivo encerrada",
"on": "Transmissão ao Vivo",
"pending": "Iniciando Transmissão ao Vivo...",
"streamIdRequired": "",
"streamIdHelp": "Aonde eu encontro isto?",
"unavailable": "",
"unavailableTitle": ""
"serviceName": "Serviço de Transmissão ao Vivo",
"signIn": "Faça login no Google",
"signInCTA": "Faça login ou insira sua chave de transmissão ao vivo do YouTube.",
"start": "Iniciar uma transmissão ao vivo",
"streamIdHelp": "O que é isso?",
"unavailableTitle": "Transmissão ao vivo indisponível"
},
"videoSIPGW": {
"busy": "Estamos trabalhando para liberar recursos. Por favor, tente novamente em alguns minutos.",
"busyTitle": "O serviço da sala está ocupado",
"errorInvite": "A conferência ainda não foi estabelecida. Por favor, tente mais tarde.",
"errorInviteTitle": "Erro no convite da sala",
"errorAlreadyInvited": "__displayName__ já convidado",
"errorInviteFailedTitle": "Convite para __displayName__ falhou",
"errorInviteFailed": "Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
"pending": "__displayName__ foi convidado",
"serviceName": "Serviço da sala",
"unavailableTitle": "Serviço da sala indisponível"
},
"speakerStats": {
"hours": "__count__h",
@@ -444,46 +426,47 @@
"selectADevice": "Selecione um dispositivo",
"testAudio": "Testar o som"
},
"invite": {
"addPassword": "Adicionar uma senha",
"callNumber": "Ligar para __number__",
"enterID": "Digite o ID da Conferência: __conferenceID__ seguido de # em um telefone para participar",
"howToDialIn": "Para participar, use um dos números a seguir e o ID da conferência",
"hidePassword": "Esconder a senha",
"inviteTo": "Convidar pessoas para __conferenceName__",
"invitedYouTo": "__userName__ o convidou para a conferência __inviteURL__",
"invitePeople": "",
"locked": "Esta chamada está travada. Novos participantes precisam ter o link e digitar a senha para entrar.",
"showPassword": "Mostrar senha",
"unlocked": "Esta chamada está destravada. Qualquer novo participante com o link pode participar."
},
"videoStatus": {
"callQuality": "Qualidade da Chamada",
"hd": "HD",
"hdTooltip": "Ver vídeo em alta definição",
"highDefinition": "Alta definição (HD)",
"labelTooltipVideo": "Qualidade do vídeo atual",
"labelTooltipAudioOnly": "Modo somente de áudio habilitado",
"labelTooiltipNoVideo": "Sem vídeo",
"labelTooltipVideo": "Qualidade do vídeo atual",
"ld": "LD",
"ldTooltip": "Ver vídeo em baixa definição",
"lowDefinition": "Baixa definição (LD)",
"onlyAudioAvailable": "Somente áudio disponível",
"onlyAudioSupported": "Suportamos somente áudio neste navegador.",
"p2pEnabled": "Ponto-a-ponto habilitada",
"p2pVideoQualityDescription": "Em modo ponto-a-ponto, qualidade de chamadas recebidas podem somente ser modificada entre alta definição e áudio somente. Outras configurações não serão honradas até sair do ponto-a-ponto.",
"recHighDefinitionOnly": "Preferência para alta definição",
"sd": "SD",
"sdTooltip": "Ver vídeo em definição padrão",
"standardDefinition": "Definição padrão",
"qualityButtonTip": "Trocar a qualidade de vídeo recebido"
},
"dialOut": {
"dial": "Discar",
"dialOut": "",
"statusMessage": "está agora __status__",
"enterPhone": "Digite o número do telefone",
"phoneNotAllowed": "Oh, ainda não temos suporte para esse destino! Desculpe!"
"statusMessage": "está agora __status__"
},
"addPeople": {
"add": "Adicionar",
"add": "Convidar",
"countryNotSupported": "Ainda não suportamos este destino.",
"countryReminder": "Ligando fora dos EUA? Por favor, certifique-se de começar com o código do país!",
"disabled": "Você não pode convidar pessoas.",
"invite": "Convidar",
"loading": "Procurando por pessoas e números de telefone",
"loadingNumber": "Validando o número de telefone",
"loadingPeople": "Procurando por pessoas para convidar",
"noResults": "Nenhum resultado de busca correspondente",
"searchPlaceholder": "Encontrar por pessoas e salas para adicionar",
"title": "Adicionar pessoas à sua chamada",
"noValidNumbers": "Por favor, digite um número de telefone",
"notAvailable": "Você não pode convidar pessoas.",
"searchNumbers": "Adicionar números de telefones",
"searchPeople": "Pesquisar pessoas",
"searchPeopleAndNumbers": "Pesquisar por pessoas ou adicionar seus números de telefone",
"telephone": "Telefone: __number__",
"title": "Convide pessoas para sua reunião",
"failedToAdd": "Falha ao adicionar membros."
},
"inlineDialogFailure": {
@@ -493,15 +476,80 @@
"supportMsg": "Se isso continuar acontecendo, chegar a"
},
"deviceError": {
"cameraError": "",
"microphoneError": "",
"cameraError": "Falha ao acessar sua câmera",
"microphoneError": "Falha ao acessar seu microfone",
"cameraPermission": "Erro ao obter permissão para a câmera",
"microphonePermission": "Erro ao obter permissão para o microfone"
},
"feedback": {
"average": "Média",
"bad": "Ruim",
"good": "Boa",
"detailsLabel": "Nos conte mais sobre isso.",
"rateExperience": "Avalie sua experiência na reunião",
"veryBad": "Muito ruim",
"veryGood": "Muito boa"
},
"info": {
"copy": "Copiar link",
"invite": "Convidar em __app__",
"title": "Informações de acesso à chamada",
"addPassword": "Adicionar uma senha",
"cancelPassword": "Cancelar senha",
"conferenceURL": "Link:",
"country": "País",
"dialANumber": "Para participar da sua reunião, disque um desses números e insira o PIN: __conferenceID__#",
"dialInNumber": "Discar:",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "Desculpe, a discagem não é atualmente suportada.",
"genericError": "Oops, alguma coisa deu errado.",
"inviteLiveStream": "Para ver a transmissão ao vivo da reunião, clique no link: __url__",
"invitePhone": "Para participar por telefone, disque __number__ e insira este PIN: __conferenceID__#",
"invitePhoneAlternatives": "Para ver mais números de telefone, clique neste link: __url__",
"inviteURL": "Para se juntar à reunião, clique neste link: __url__",
"liveStreamURL": "Transmissão ao vivo:",
"moreNumbers": "Mais números",
"noNumbers": "Sem números de discagem.",
"noPassword": "Nenhum",
"noRoom": "Nenhuma sala foi especificada para entrar.",
"numbers": "Números de discagem",
"password": "Senha:",
"title": "Compartilhar",
"tooltip": "Obtenha informações de acesso sobre a reunião"
},
"settingsView": {
"alertOk": "OK",
"alertTitle": "Atenção",
"alertURLText": "A URL digitada do servidor é inválida",
"conferenceSection": "Conferência",
"displayName": "Nome de exibição",
"email": "E-mail",
"header": "Configurações",
"profileSection": "Perfil",
"serverURL": "URL do servidor",
"startWithAudioMuted": "Iniciar sem áudio",
"startWithVideoMuted": "Iniciar sem vídeo"
},
"calendarSync": {
"later": "Depois",
"next": "Recebendo",
"nextMeeting": "próxima reunião",
"now": "Agora",
"permissionButton": "Abrir configurações",
"permissionMessage": "Permissão do calendário é requerida para listar suas reuniões na aplicação."
},
"recentList": {
"today": "Hoje",
"yesterday": "Ontem",
"earlier": "Mais cedo"
},
"sectionList": {
"pullToRefresh": "Puxe para atualizar"
},
"deepLinking": {
"title": "Iniciando sua reunião no __app__...",
"description": "Nada acontece? Estamos tentando iniciar sua reunião no aplicativo desktop __app__. Tente novamente ou inicie ele na aplicação web __app__.",
"tryAgainButton": "Tente novamente no desktop",
"launchWebButton": "Iniciar na web",
"appNotInstalled": "Você precisa do aplicativo móvel __app__ para participar da reunião no seu telefone.",
"downloadApp": "Baixe o Aplicativo",
"openApp": "Continue na aplicação"
}
}

View File

@@ -626,9 +626,7 @@
"permissionMessage": "The Calendar permission is required to see your meetings in the app."
},
"recentList": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
"joinPastMeeting": "Join A Past Meeting"
},
"sectionList": {
"pullToRefresh": "Pull to refresh"
@@ -656,6 +654,11 @@
"ignored": "Ignored",
"expired": "Expired"
},
"dateUtils": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
},
"incomingCall": {
"answer": "Answer",
"audioCallTitle": "Incoming call",

View File

@@ -1,7 +1,6 @@
/* eslint-disable no-unused-vars, no-var */
// Logging configuration
// XXX When making any changes to this file make sure to also update it's React
// version at ./react/features/base/logging/reducer.js !!!
var loggingConfig = {
// default log level for the app and lib-jitsi-meet
defaultLogLevel: 'trace',
@@ -11,9 +10,17 @@ var loggingConfig = {
// The following are too verbose in their logging with the
// {@link #defaultLogLevel}:
'modules/RTC/TraceablePeerConnection.js': 'info',
'modules/statistics/CallStats.js': 'info',
'modules/xmpp/strophe.util.js': 'log',
'modules/RTC/TraceablePeerConnection.js': 'info'
'modules/xmpp/strophe.util.js': 'log'
};
/* eslint-enable no-unused-vars, no-var */
// XXX Web/React server-includes logging_config.js into index.html.
// Mobile/react-native requires it in react/features/base/logging. For the
// purposes of the latter, (try to) export loggingConfig. The following
// detection of a module system is inspired by webpack.
typeof module === 'object'
&& typeof exports === 'object'
&& (module.exports = loggingConfig);

View File

@@ -112,11 +112,21 @@ function initCommands() {
const { name } = request;
switch (name) {
case 'invite':
case 'invite': // eslint-disable-line no-case-declarations
const { invitees } = request;
if (!Array.isArray(invitees) || invitees.length === 0) {
callback({
error: new Error('Unexpected format of invitees')
});
break;
}
// The store should be already available because API.init is called
// on appWillMount action.
APP.store.dispatch(
invite(request.invitees, true))
invite(invitees, true))
.then(failedInvitees => {
let error;
let result;

View File

@@ -238,7 +238,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
})
});
this.invite(invitees);
if (Array.isArray(invitees) && invitees.length > 0) {
this.invite(invitees);
}
this._isLargeVideoVisible = true;
this._numberOfParticipants = 0;
this._participants = {};
@@ -597,6 +599,10 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* @returns {Promise} - Resolves on success and rejects on failure.
*/
invite(invitees) {
if (!Array.isArray(invitees) || invitees.length === 0) {
return Promise.reject(new TypeError('Invalid Argument'));
}
return this._transport.sendRequest({
name: 'invite',
invitees

View File

@@ -163,6 +163,8 @@ RemoteVideo.prototype._generatePopupContent = function() {
const onVolumeChange = this._setAudioVolume;
const { isModerator } = APP.conference;
const participantID = this.id;
const menuPosition = interfaceConfig.VERTICAL_FILMSTRIP
? 'left bottom' : 'top center';
ReactDOM.render(
<Provider store = { APP.store }>
@@ -172,6 +174,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
initialVolumeValue = { initialVolumeValue }
isAudioMuted = { this.isAudioMuted }
isModerator = { isModerator }
menuPosition = { menuPosition }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }

View File

@@ -854,6 +854,9 @@ const VideoLayout = {
resizeVideoArea(
forceUpdate = false,
animate = false) {
// Resize the thumbnails first.
this.resizeThumbnails(forceUpdate);
if (largeVideo) {
largeVideo.updateContainerSize();
largeVideo.resize(animate);
@@ -866,9 +869,6 @@ const VideoLayout = {
if (availableWidth < 0 || availableHeight < 0) {
return;
}
// Resize the thumbnails first.
this.resizeThumbnails(forceUpdate);
},
getSmallVideo(id) {

52
package-lock.json generated
View File

@@ -5573,11 +5573,6 @@
"randomfill": "^1.0.3"
}
},
"crypto-js": {
"version": "3.1.9-1",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz",
"integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg="
},
"css-color-list": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/css-color-list/-/css-color-list-0.0.1.tgz",
@@ -5732,6 +5727,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
"integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
"dev": true,
"requires": {
"es5-ext": "^0.10.9"
}
@@ -6236,6 +6232,7 @@
"version": "0.10.39",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.39.tgz",
"integrity": "sha512-AlaXZhPHl0po/uxMx1tyrlt1O86M6D5iVaDH8UgLfgek4kXTX6vzsRfJQWC2Ku+aG8pkw1XWzh9eTkwfVrsD5g==",
"dev": true,
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.1"
@@ -6245,6 +6242,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"dev": true,
"requires": {
"d": "1",
"es5-ext": "^0.10.35",
@@ -6282,6 +6280,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
"integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
"dev": true,
"requires": {
"d": "1",
"es5-ext": "~0.10.14"
@@ -9524,6 +9523,11 @@
"dev": true,
"optional": true
},
"jsc-android": {
"version": "224109.1.0",
"resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-224109.1.0.tgz",
"integrity": "sha512-mhALFynePc/wJsUt9BJuH13mSK/dGWtBO/pcYwVV1I0A7iduyqy3fSoAt1b0yI+/B3TzlGyue/gqjPxsqG1HRQ=="
},
"jsesc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
@@ -10507,6 +10511,11 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.19.4.tgz",
"integrity": "sha512-1xFTAknSLfc47DIxHDUbnJWC+UwgWxATmymaxIPQpmMh7LBm7ZbwVEsuushqwL2GYZU0jie4xO+TK44hJPjNSQ=="
},
"moment-duration-format": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/moment-duration-format/-/moment-duration-format-2.2.2.tgz",
"integrity": "sha1-uVdhLeJgFsmtnrYIfAVFc+USd3k="
},
"morgan": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz",
@@ -12715,35 +12724,12 @@
"jssha": "^2.2.0"
}
},
"react-native-fetch-blob": {
"version": "github:joltup/react-native-fetch-blob#1f9a1761aea4e37bd672bd0d233f3adf0e113a11",
"from": "github:joltup/react-native-fetch-blob#1f9a1761aea4e37bd672bd0d233f3adf0e113a11",
"react-native-fast-image": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-4.0.14.tgz",
"integrity": "sha512-MeRgL70JxoY/hn8ZRGBsDED9SGvTEeznneL//fWZyLaG0CM+w2CH4QXAMvADnIvu2RFd8WQWNii6c6VOpVe4Tg==",
"requires": {
"base-64": "0.1.0",
"glob": "7.0.6"
},
"dependencies": {
"glob": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
"integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.2",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"react-native-img-cache": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/react-native-img-cache/-/react-native-img-cache-1.5.2.tgz",
"integrity": "sha1-6HG4MJk3t/mSbgmwFuTk5nWKOfo=",
"requires": {
"crypto-js": "^3.1.9-1"
"prop-types": "^15.5.10"
}
},
"react-native-immersive": {

View File

@@ -36,8 +36,6 @@
"@atlaskit/tooltip": "9.1.1",
"@webcomponents/url": "0.7.1",
"autosize": "1.18.13",
"es6-iterator": "2.0.3",
"es6-symbol": "3.1.1",
"i18next": "8.4.3",
"i18next-browser-languagedetector": "2.0.0",
"i18next-xhr-backend": "1.4.2",
@@ -47,10 +45,12 @@
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"jsc-android": "224109.1.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
"lodash": "4.17.4",
"moment": "2.19.4",
"moment-duration-format": "2.2.2",
"postis": "2.2.0",
"prop-types": "15.6.0",
"react": "16.3.1",
@@ -60,8 +60,7 @@
"react-native-background-timer": "2.0.0",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9",
"react-native-callstats": "3.52.0",
"react-native-fetch-blob": "github:joltup/react-native-fetch-blob#1f9a1761aea4e37bd672bd0d233f3adf0e113a11",
"react-native-img-cache": "1.5.2",
"react-native-fast-image": "4.0.14",
"react-native-immersive": "1.1.0",
"react-native-keep-awake": "2.0.6",
"react-native-linear-gradient": "2.4.0",

View File

@@ -4,6 +4,7 @@ import { AbstractAudioMuteButton } from '../base/toolbox';
import type { AbstractButtonProps as Props } from '../base/toolbox';
const { api } = window.alwaysOnTop;
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The type of the React {@code Component} state of {@link AudioMuteButton}.
@@ -68,7 +69,7 @@ export default class AudioMuteButton
audioAvailable,
audioMuted
}))
.catch(console.error);
.catch(logger.error);
}
/**

View File

@@ -4,6 +4,7 @@ import { AbstractVideoMuteButton } from '../base/toolbox';
import type { AbstractButtonProps as Props } from '../base/toolbox';
const { api } = window.alwaysOnTop;
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The type of the React {@code Component} state of {@link VideoMuteButton}.
@@ -68,7 +69,7 @@ export default class VideoMuteButton
videoAvailable,
videoMuted
}))
.catch(console.error);
.catch(logger.error);
}
/**

View File

@@ -11,6 +11,7 @@ import {
AspectRatioDetector,
ReducedUIDetector
} from '../../base/responsive-ui';
import '../../google-api';
import '../../mobile/audio-mode';
import '../../mobile/background';
import '../../mobile/callkit';
@@ -26,6 +27,8 @@ import type { Props as AbstractAppProps } from './AbstractApp';
declare var __DEV__;
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The type of React {@code Component} props of {@link App}.
*/
@@ -196,7 +199,7 @@ function _handleException(error, fatal) {
// an unhandled JavascriptException for an unhandled JavaScript error.
// This will effectively kill the app. In accord with the Web, do not
// kill the app.
console.error(error);
logger.error(error);
} else {
// Forward to the next globalHandler of ErrorUtils.
const { next } = _handleException;

View File

@@ -11,6 +11,8 @@ import {
JITSI_CONFERENCE_URL_KEY
} from './constants';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Attach a set of local tracks to a conference.
*
@@ -172,7 +174,7 @@ export function _removeLocalTracksFromConference(
function _reportError(msg, err) {
// TODO This is a good point to call some global error handler when we have
// one.
console.error(msg, err);
logger.error(msg, err);
}
/**

View File

@@ -24,12 +24,14 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import {
conferenceFailed,
conferenceLeft,
conferenceWillLeave,
createConference,
setLastN
} from './actions';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY,
SET_LASTN,
@@ -46,6 +48,11 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
declare var APP: Object;
/**
* Handler for before unload event.
*/
let beforeUnloadHandler;
/**
* Implements the middleware of the feature base/conference.
*
@@ -66,6 +73,10 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_FAILED:
return _connectionFailed(store, next, action);
case CONFERENCE_WILL_LEAVE:
_conferenceWillLeave();
break;
case DATA_CHANNEL_OPENED:
return _syncReceiveVideoQuality(store, next, action);
@@ -135,6 +146,11 @@ function _conferenceFailed(store, next, action) {
// conference is handled by /conference.js and appropriate failure handlers
// are set there.
if (typeof APP !== 'undefined') {
if (typeof beforeUnloadHandler !== 'undefined') {
window.removeEventListener('beforeunload', beforeUnloadHandler);
beforeUnloadHandler = undefined;
}
return result;
}
@@ -175,6 +191,16 @@ function _conferenceJoined({ dispatch, getState }, next, action) {
// and the LastN value needs to be synchronized here.
audioOnly && conference.getLastN() !== 0 && dispatch(setLastN(0));
// FIXME: Very dirty solution. This will work on web only.
// When the user closes the window or quits the browser, lib-jitsi-meet
// handles the process of leaving the conference. This is temporary solution
// that should cover the described use case as part of the effort to
// implement the conferenceWillLeave action for web.
beforeUnloadHandler = () => {
dispatch(conferenceWillLeave(conference));
};
window.addEventListener('beforeunload', beforeUnloadHandler);
return result;
}
@@ -227,6 +253,11 @@ function _connectionFailed({ dispatch, getState }, next, action) {
const result = next(action);
if (typeof beforeUnloadHandler !== 'undefined') {
window.removeEventListener('beforeunload', beforeUnloadHandler);
beforeUnloadHandler = undefined;
}
// FIXME: Workaround for the web version. Currently, the creation of the
// conference is handled by /conference.js and appropriate failure handlers
// are set there.
@@ -266,6 +297,21 @@ function _connectionFailed({ dispatch, getState }, next, action) {
return result;
}
/**
* Notifies the feature base/conference that the action
* {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
* store.
*
* @private
* @returns {void}
*/
function _conferenceWillLeave() {
if (typeof beforeUnloadHandler !== 'undefined') {
window.removeEventListener('beforeunload', beforeUnloadHandler);
beforeUnloadHandler = undefined;
}
}
/**
* Returns whether or not a CONNECTION_FAILED action is for a possible split
* brain error. A split brain error occurs when at least two users join a
@@ -447,7 +493,7 @@ function _setLastN({ getState }, next, action) {
try {
conference.setLastN(action.lastN);
} catch (err) {
console.error(`Failed to set lastN: ${err}`);
logger.error(`Failed to set lastN: ${err}`);
}
}

View File

@@ -37,6 +37,9 @@ const INITIAL_RN_STATE = {
// fastest to merely disable them.
disableAudioLevels: true,
// FIXME flow complains about missing 'locationURL' missing in _setConfig
locationURL: undefined,
p2p: {
disableH264: false,
preferH264: true
@@ -126,8 +129,10 @@ function _setConfig(state, { config }) {
const newState = _.merge(
{},
config,
{ error: undefined },
config, {
error: undefined,
locationURL: state.locationURL
},
// The config of _getInitialState() is meant to override the config
// downloaded from the Jitsi Meet deployment because the former contains

View File

@@ -9,17 +9,6 @@
*/
export const SET_AUDIO_INPUT_DEVICE = Symbol('SET_AUDIO_INPUT_DEVICE');
/**
* The type of Redux action which signals that the currently used audio
* output device should be changed.
*
* {
* type: SET_AUDIO_OUTPUT_DEVICE,
* deviceId: string,
* }
*/
export const SET_AUDIO_OUTPUT_DEVICE = Symbol('SET_AUDIO_OUTPUT_DEVICE');
/**
* The type of Redux action which signals that the currently used video
* input device should be changed.

View File

@@ -1,10 +1,34 @@
import JitsiMeetJS from '../lib-jitsi-meet';
import {
SET_AUDIO_INPUT_DEVICE,
SET_AUDIO_OUTPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE,
UPDATE_DEVICE_LIST
} from './actionTypes';
/**
* Queries for connected A/V input and output devices and updates the redux
* state of known devices.
*
* @returns {Function}
*/
export function getAvailableDevices() {
return dispatch => new Promise(resolve => {
const { mediaDevices } = JitsiMeetJS;
if (mediaDevices.isDeviceListAvailable()
&& mediaDevices.isDeviceChangeAvailable()) {
mediaDevices.enumerateDevices(devices => {
dispatch(updateDeviceList(devices));
resolve(devices);
});
} else {
resolve([]);
}
});
}
/**
* Signals to update the currently used audio input device.
*
@@ -21,22 +45,6 @@ export function setAudioInputDevice(deviceId) {
};
}
/**
* Signals to update the currently used audio output device.
*
* @param {string} deviceId - The id of the new audio ouput device.
* @returns {{
* type: SET_AUDIO_OUTPUT_DEVICE,
* deviceId: string
* }}
*/
export function setAudioOutputDevice(deviceId) {
return {
type: SET_AUDIO_OUTPUT_DEVICE,
deviceId
};
}
/**
* Signals to update the currently used video input device.
*

View File

@@ -6,7 +6,6 @@ import { MiddlewareRegistry } from '../redux';
import {
SET_AUDIO_INPUT_DEVICE,
SET_AUDIO_OUTPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE
} from './actionTypes';
@@ -22,9 +21,6 @@ MiddlewareRegistry.register(store => next => action => {
case SET_AUDIO_INPUT_DEVICE:
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
break;
case SET_AUDIO_OUTPUT_DEVICE:
APP.UI.emitEvent(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, action.deviceId);
break;
case SET_VIDEO_INPUT_DEVICE:
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
break;

View File

@@ -1,6 +1,5 @@
import {
SET_AUDIO_INPUT_DEVICE,
SET_AUDIO_OUTPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE,
UPDATE_DEVICE_LIST
} from './actionTypes';
@@ -40,7 +39,6 @@ ReducerRegistry.register(
// now.
case SET_AUDIO_INPUT_DEVICE:
case SET_VIDEO_INPUT_DEVICE:
case SET_AUDIO_OUTPUT_DEVICE:
default:
return state;
}

View File

@@ -103,6 +103,28 @@ class DialogWithTabs extends Component<Props, State> {
);
}
/**
* Gets the props to pass into the tab component.
*
* @param {number} tabId - The index of the tab configuration within
* {@link this.state.tabStates}.
* @returns {Object}
*/
_getTabProps(tabId) {
const { tabs } = this.props;
const { tabStates } = this.state;
const tabConfiguration = tabs[tabId];
const currentTabState = tabStates[tabId];
if (tabConfiguration.propsUpdateFunction) {
return tabConfiguration.propsUpdateFunction(
currentTabState,
tabConfiguration.props);
}
return { ...currentTabState };
}
/**
* Renders the tabs from the tab information passed on props.
*
@@ -155,10 +177,11 @@ class DialogWithTabs extends Component<Props, State> {
<div className = { styles }>
<TabComponent
closeDialog = { closeDialog }
mountCallback = { this.props.tabs[tabId].onMount }
onTabStateChange
= { this._onTabStateChange }
tabId = { tabId }
{ ...this.state.tabStates[tabId] } />
{ ...this._getTabProps(tabId) } />
</div>);
}

View File

@@ -4,6 +4,9 @@ import moment from 'moment';
import i18next from './i18next';
// allows for moment durations to be formatted
import 'moment-duration-format';
// MomentJS uses static language bundle loading, so in order to support dynamic
// language selection in the app we need to load all bundles that we support in
// the app.
@@ -55,8 +58,19 @@ export function getLocalizedDurationFormatter(duration: number) {
// states v2.19 so maybe locale on moment's duration was introduced in
// between?
//
// If the conference is under an hour long we want to display it without
// showing the hour and we want to include the hour if the conference is
// more than an hour long
// $FlowFixMe
return moment.duration(duration).locale(_getSupportedLocale());
if (moment.duration(duration).format('h') !== '0') {
// $FlowFixMe
return moment.duration(duration).format('h:mm:ss');
}
// $FlowFixMe
return moment.duration(duration).format('mm:ss', { trim: false });
}
/**

View File

@@ -10,6 +10,8 @@ declare var APP: Object;
const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Creates a {@link JitsiLocalTrack} model from the given device id.
*
@@ -133,7 +135,7 @@ export function loadConfig(
return config;
})
.catch(err => {
console.error(`Failed to load config from ${url}`, err);
logger.error(`Failed to load config from ${url}`, err);
throw err;
});

View File

@@ -3,6 +3,8 @@
import { NativeModules } from 'react-native';
import { RTCPeerConnection, RTCSessionDescription } from 'react-native-webrtc';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/* eslint-disable no-unused-vars */
// Address families.
@@ -140,7 +142,7 @@ _RTCPeerConnection.prototype.setRemoteDescription = function(
* @returns {void}
*/
function _LOGE(...args) {
console && console.error && console.error(...args);
logger.error(...args);
}
/**

View File

@@ -1,4 +1,3 @@
import Iterator from 'es6-iterator';
import BackgroundTimer from 'react-native-background-timer';
import '@webcomponents/url'; // Polyfill for URL constructor
@@ -114,16 +113,13 @@ function _visitNode(node, callback) {
global.addEventListener = () => {};
}
// Array.prototype[@@iterator]
// removeEventListener
//
// Required by:
// - for...of statement use(s) in lib-jitsi-meet
const arrayPrototype = Array.prototype;
if (typeof arrayPrototype['@@iterator'] === 'undefined') {
arrayPrototype['@@iterator'] = function() {
return new Iterator(this);
};
// - features/base/conference/middleware
if (typeof global.removeEventListener === 'undefined') {
// eslint-disable-next-line no-empty-function
global.removeEventListener = () => {};
}
// document

View File

@@ -7,23 +7,12 @@ import { SET_LOGGING_CONFIG } from './actionTypes';
/**
* The default/initial redux state of the feature base/logging.
*
* XXX When making any changes to the DEFAULT_STATE make sure to also update
* logging_config.js file located in the root directory of this project !!!
*
* @type {{
* config: Object
* }}
*/
const DEFAULT_STATE = {
config: {
defaultLogLevel: 'trace',
// The following are too verbose in their logging with the
// {@link #defaultLogLevel}:
'modules/statistics/CallStats.js': 'info',
'modules/xmpp/strophe.util.js': 'log',
'modules/RTC/TraceablePeerConnection.js': 'info'
}
config: require('../../../../logging_config.js')
};
ReducerRegistry.register(

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Animated, View } from 'react-native';
import { View } from 'react-native';
import { connect } from 'react-redux';
import AbstractVideoTrack from '../AbstractVideoTrack';
@@ -19,131 +19,18 @@ class VideoTrack extends AbstractVideoTrack {
static propTypes = AbstractVideoTrack.propTypes
/**
* Initializes a new VideoTrack instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
/**
* Reference to currently running animation if any.
*
* @private
*/
this._animation = null;
/**
* Extend Component's state with additional animation-related vars.
*
* @type {Object}
*/
this.state = {
...this.state,
fade: new Animated.Value(0)
};
}
/**
* Renders video element with animation.
* Renders the video element for the associated video track.
*
* @override
* @returns {ReactElement}
*/
render() {
const animatedStyles
= [ styles.videoCover, this._getAnimationStyles() ];
return (
<View style = { styles.video } >
{ super.render() }
<Animated.View
pointerEvents = 'none'
style = { animatedStyles } />
</View>
);
}
/**
* Animates setting a new video track to be rendered by this instance.
*
* @param {Track} oldValue - The old video track rendered by this instance.
* @param {Track} newValue - The new video track to be rendered by this
* instance.
* @private
* @returns {Promise}
*/
_animateSetVideoTrack(oldValue, newValue) {
// If we're in the middle of an animation and a new animation is about
// to start, stop the previous one first.
if (this._animation) {
this._animation.stop();
this._animation = null;
this.state.fade.setValue(0);
}
return this._animateVideoTrack(1)
.then(() => {
super._setVideoTrack(newValue);
return this._animateVideoTrack(0);
})
.catch(() => console.log('Animation was stopped'));
}
/**
* Animates the display of the state videoTrack.
*
* @param {number} toValue - The value to which the specified animatedValue
* is to be animated.
* @private
* @returns {Promise}
*/
_animateVideoTrack(toValue) {
return new Promise((resolve, reject) => {
this._animation
= Animated.timing(this.state.fade, { toValue });
this._animation.start(result => {
this._animation = null;
result.finished ? resolve() : reject();
});
});
}
/**
* Returns animation styles for Animated.View.
*
* @private
* @returns {Object}
*/
_getAnimationStyles() {
return {
opacity: this.state.fade
};
}
// eslint-disable-next-line valid-jsdoc
/**
* Animate the setting of the video track to be rendered by this instance.
*
* @inheritdoc
* @protected
*/
_setVideoTrack(videoTrack) {
// If JitsiTrack instance didn't change, that means some other track's
// props were changed and we don't need to animate.
const oldValue = this.state.videoTrack;
const oldJitsiTrack = oldValue ? oldValue.jitsiTrack : null;
const newValue = videoTrack;
const newJitsiTrack = newValue ? newValue.jitsiTrack : null;
if (oldJitsiTrack === newJitsiTrack) {
super._setVideoTrack(newValue);
} else {
this._animateSetVideoTrack(oldValue, newValue);
}
}
}
export default connect()(VideoTrack);

View File

@@ -229,7 +229,7 @@ class VideoTransform extends Component<Props, State> {
onLayout = { this._onLayout }
pointerEvents = 'box-only'
style = { [
styles.videoTransformedViewContaier,
styles.videoTransformedViewContainer,
style
] }
{ ...this.gestureHandlers.panHandlers }>

View File

@@ -1,7 +1,5 @@
import { StyleSheet } from 'react-native';
import { ColorPalette } from '../../../styles';
/**
* The styles of the feature base/media.
*/
@@ -19,7 +17,7 @@ export default StyleSheet.create({
* that can be visible on special occasions, such as during device rotate
* animation, or PiP mode.
*/
videoTransformedViewContaier: {
videoTransformedViewContainer: {
overflow: 'hidden'
},
@@ -28,14 +26,5 @@ export default StyleSheet.create({
*/
video: {
flex: 1
},
/**
* Black cover for the video, which will be animated by reducing its opacity
* and create a fade-in effect.
*/
videoCover: {
...StyleSheet.absoluteFillObject,
backgroundColor: ColorPalette.black
}
});

View File

@@ -1,10 +1,9 @@
// @flow
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import { Image, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import { CachedImage, ImageCache } from '../../../mobile/image-cache';
import { Platform } from '../../react';
import { ColorPalette } from '../../styles';
import styles from './styles';
@@ -46,7 +45,8 @@ type Props = {
*/
type State = {
backgroundColor: string,
source: number | { uri: string }
source: ?{ uri: string },
useDefaultAvatar: boolean
};
/**
@@ -68,6 +68,9 @@ export default class Avatar extends Component<Props, State> {
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onAvatarLoaded = this._onAvatarLoaded.bind(this);
// Fork (in Facebook/React speak) the prop uri because Image will
// receive it through a source object. Additionally, other props may be
// forked as well.
@@ -94,18 +97,8 @@ export default class Avatar extends Component<Props, State> {
if (prevURI !== nextURI || assignState) {
const nextState = {
backgroundColor: this._getBackgroundColor(nextProps),
/**
* The source of the {@link Image} which is the actual
* representation of this {@link Avatar}. The state
* {@code source} was explicitly introduced in order to reduce
* unnecessary renders.
*
* @type {{
* uri: string
* }}
*/
source: _DEFAULT_SOURCE
source: undefined,
useDefaultAvatar: true
};
if (assignState) {
@@ -130,7 +123,14 @@ export default class Avatar extends Component<Props, State> {
// an image retrieval action.
if (nextURI && !nextURI.startsWith('#')) {
const nextSource = { uri: nextURI };
const observer = () => {
if (assignState) {
// eslint-disable-next-line react/no-direct-mutation-state
this.state = {
...this.state,
source: nextSource
};
} else {
this._unmounted || this.setState((prevState, props) => {
if (props.uri === nextURI
&& (!prevState.source
@@ -140,22 +140,6 @@ export default class Avatar extends Component<Props, State> {
return {};
});
};
// Wait for the source/URI to load.
if (ImageCache) {
ImageCache.get().on(
nextSource,
observer,
/* immutable */ true);
} else if (assignState) {
// eslint-disable-next-line react/no-direct-mutation-state
this.state = {
...this.state,
source: nextSource
};
} else {
observer();
}
}
}
@@ -186,131 +170,132 @@ export default class Avatar extends Component<Props, State> {
*/
_getBackgroundColor({ uri }) {
if (!uri) {
// @lyubomir: I'm leaving @saghul's implementation which picks up a
// random color bellow so that we have it in the source code in
// case we decide to use it in the future. However, I think at the
// time of this writing that the randomness reduces the
// predictability which React is supposed to bring to our app.
return ColorPalette.white;
}
let hash = 0;
if (typeof uri === 'string') {
/* eslint-disable no-bitwise */
/* eslint-disable no-bitwise */
for (let i = 0; i < uri.length; i++) {
hash = uri.charCodeAt(i) + ((hash << 5) - hash);
hash |= 0; // Convert to 32-bit integer
}
/* eslint-enable no-bitwise */
} else {
// @saghul: If we have no URI yet, we have no data to hash from. So
// use a random value.
hash = Math.floor(Math.random() * 360);
for (let i = 0; i < uri.length; i++) {
hash = uri.charCodeAt(i) + ((hash << 5) - hash);
hash |= 0; // Convert to 32-bit integer
}
/* eslint-enable no-bitwise */
return `hsl(${hash % 360}, 100%, 75%)`;
}
/**
* Helper which computes the style for the {@code Image} / {@code FastImage}
* component.
*
* @private
* @returns {Object}
*/
_getImageStyle() {
const { size } = this.props;
return {
...styles.avatar,
borderRadius: size / 2,
height: size,
width: size
};
}
_onAvatarLoaded: () => void;
/**
* Handler called when the remote image was loaded. When this happens we
* show that instead of the default locally generated one.
*
* @private
* @returns {void}
*/
_onAvatarLoaded() {
this._unmounted || this.setState({ useDefaultAvatar: false });
}
/**
* Renders a default, locally generated avatar image.
*
* @private
* @returns {ReactElement}
*/
_renderDefaultAvatar() {
// When using a local image, react-native-fastimage falls back to a
// regular Image, so we need to wrap it in a view to make it round.
// https://github.com/facebook/react-native/issues/3198
const { backgroundColor, useDefaultAvatar } = this.state;
const imageStyle = this._getImageStyle();
const viewStyle = {
...imageStyle,
backgroundColor,
display: useDefaultAvatar ? 'flex' : 'none',
// FIXME @lyubomir: Without the opacity bellow I feel like the
// avatar colors are too strong. Besides, we use opacity for the
// ToolbarButtons. That's where I copied the value from and we
// may want to think about "standardizing" the opacity in the
// app in a way similar to ColorPalette.
opacity: 0.1,
overflow: 'hidden'
};
return (
<View style = { viewStyle }>
<Image
// The Image adds a fade effect without asking, so lets
// explicitly disable it. More info here:
// https://github.com/facebook/react-native/issues/10194
fadeDuration = { 0 }
resizeMode = 'contain'
source = { _DEFAULT_SOURCE }
style = { imageStyle } />
</View>
);
}
/**
* Renders an avatar using a remote image.
*
* @private
* @returns {ReactElement}
*/
_renderAvatar() {
const { source, useDefaultAvatar } = this.state;
const style = {
...this._getImageStyle(),
display: useDefaultAvatar ? 'none' : 'flex'
};
return (
<FastImage
onLoad = { this._onAvatarLoaded }
resizeMode = 'contain'
source = { source }
style = { style } />
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
// Propagate all props of this Avatar but the ones consumed by this
// Avatar to the Image it renders.
const {
/* eslint-disable no-unused-vars */
const { source, useDefaultAvatar } = this.state;
// The following are forked in state:
uri: forked0,
/* eslint-enable no-unused-vars */
size,
...props
} = this.props;
const {
backgroundColor,
source
} = this.state;
// Compute the base style
const borderRadius = size / 2;
const style = {
...styles.avatar,
// XXX Workaround for Android: for radii < 80 the border radius
// doesn't work properly, but applying a radius twice as big seems
// to do the trick.
borderRadius:
Platform.OS === 'android' && borderRadius < 80
? size * 2
: borderRadius,
height: size,
width: size
};
// If we're rendering the _DEFAULT_SOURCE, then we want to do some
// additional fu like having automagical colors generated per
// participant, transparency to make the intermediate state while
// downloading the remote image a little less "in your face", etc.
let styleWithBackgroundColor;
if (source === _DEFAULT_SOURCE && backgroundColor) {
styleWithBackgroundColor = {
...style,
backgroundColor,
// FIXME @lyubomir: Without the opacity bellow I feel like the
// avatar colors are too strong. Besides, we use opacity for the
// ToolbarButtons. That's where I copied the value from and we
// may want to think about "standardizing" the opacity in the
// app in a way similar to ColorPalette.
opacity: 0.1,
overflow: 'hidden'
};
}
// If we're styling with backgroundColor, we need to wrap the Image in a
// View because of a bug in React Native for Android:
// https://github.com/facebook/react-native/issues/3198
let imageStyle;
let viewStyle;
if (styleWithBackgroundColor) {
if (Platform.OS === 'android') {
imageStyle = style;
viewStyle = styleWithBackgroundColor;
} else {
imageStyle = styleWithBackgroundColor;
}
} else {
imageStyle = style;
}
let element
= React.createElement(
// XXX CachedImage removed support for images which clearly do
// not need caching.
typeof source === 'number' ? Image : CachedImage,
{
...props,
resizeMode: 'contain',
source,
style: imageStyle
});
if (viewStyle) {
element = React.createElement(View, { style: viewStyle }, element);
}
return element;
return (
<Fragment>
{ source && this._renderAvatar() }
{ useDefaultAvatar && this._renderDefaultAvatar() }
</Fragment>
);
}
}

View File

@@ -2,6 +2,7 @@
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import { connect } from 'react-redux';
import { translate } from '../../i18n';
@@ -11,7 +12,6 @@ import {
shouldRenderVideoTrack,
VideoTrack
} from '../../media';
import { prefetch } from '../../../mobile/image-cache';
import { Container, TintedView } from '../../react';
import { TestHint } from '../../testing/components';
import { getTrackByMediaTypeAndParticipant } from '../../tracks';
@@ -303,7 +303,8 @@ function _mapStateToProps(state, ownProps) {
// ParticipantView knows before Avatar that an avatar URL will be used
// so it's advisable to prefetch here.
avatar && prefetch({ uri: avatar });
avatar && !avatar.startsWith('#')
&& FastImage.preload([ { uri: avatar } ]);
}
return {

View File

@@ -0,0 +1,75 @@
// @flow
import type { ComponentType, Element } from 'react';
/**
* Item data for <tt>NavigateSectionList</tt>.
*/
export type Item = {
/**
* the color base of the avatar
*/
colorBase: string,
/**
* Item title
*/
title: string,
/**
* Item url
*/
url: string,
/**
* lines[0] - date
* lines[1] - duration
* lines[2] - server name
*/
lines: Array<string>
}
/**
* web implementation of section data for NavigateSectionList
*/
export type Section = {
/**
* section title
*/
title: string,
/**
* unique key for the section
*/
key?: string,
/**
* Array of items in the section
*/
data: $ReadOnlyArray<Item>,
/**
* Optional properties added only to fix some flow errors thrown by React
* SectionList
*/
ItemSeparatorComponent?: ?ComponentType<any>,
keyExtractor?: (item: Object) => string,
renderItem?: ?(info: Object) => ?Element<any>
}
/**
* native implementation of section data for NavigateSectionList
*
* When react-native's SectionList component parses through an array of sections
* it passes the section nested within the section property of another object
* to the renderSection method (on web for our own implementation of SectionList
* this nesting is not implemented as there is no need for nesting)
*/
export type SetionListSection = {
section: Section
}

View File

@@ -0,0 +1,223 @@
// @flow
import React, { Component } from 'react';
// TODO: Maybe try to make all NavigateSectionList components to work for both
// mobile and web, and move them to NavigateSectionList component.
import {
NavigateSectionListEmptyComponent,
NavigateSectionListItem,
NavigateSectionListSectionHeader,
SectionList
} from './_';
import type { Section } from '../Types';
type Props = {
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
onPress: Function,
/**
* Function to be invoked when pull-to-refresh is performed.
*/
onRefresh: Function,
/**
* Function to override the rendered default empty list component.
*/
renderListEmptyComponent: Function,
/**
* An array of sections
*/
sections: Array<Section>
};
/**
* Implements a general section list to display items that have a URL property
* and navigates to (probably) meetings, such as the recent list or the meeting
* list components.
*/
class NavigateSectionList extends Component<Props> {
/**
* Creates an empty section object.
*
* @param {string} title - The title of the section.
* @param {string} key - The key of the section. It must be unique.
* @private
* @returns {Object}
*/
static createSection(title: string, key: string) {
return {
data: [],
key,
title
};
}
/**
* Constructor of the NavigateSectionList component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._getItemKey = this._getItemKey.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._renderItem = this._renderItem.bind(this);
this._renderListEmptyComponent
= this._renderListEmptyComponent.bind(this);
this._renderSectionHeader = this._renderSectionHeader.bind(this);
}
/**
* Implements React's Component.render.
* Note: we don't use the refreshing value yet, because refreshing of these
* lists is super quick, no need to complicate the code - yet.
*
* @inheritdoc
*/
render() {
const {
renderListEmptyComponent = this._renderListEmptyComponent,
sections
} = this.props;
return (
<SectionList
ListEmptyComponent = { renderListEmptyComponent }
keyExtractor = { this._getItemKey }
onItemClick = { this.props.onPress }
onRefresh = { this._onRefresh }
refreshing = { false }
renderItem = { this._renderItem }
renderSectionHeader = { this._renderSectionHeader }
sections = { sections } />
);
}
_getItemKey: (Object, number) => string;
/**
* Generates a unique id to every item.
*
* @param {Object} item - The item.
* @param {number} index - The item index.
* @private
* @returns {string}
*/
_getItemKey(item, index) {
return `${index}-${item.key}`;
}
_onPress: string => Function;
/**
* Returns a function that is used in the onPress callback of the items.
*
* @param {string} url - The URL of the item to navigate to.
* @private
* @returns {Function}
*/
_onPress(url) {
return () => {
const { disabled, onPress } = this.props;
!disabled && url && typeof onPress === 'function' && onPress(url);
};
}
_onRefresh: () => void;
/**
* Invokes the onRefresh callback if present.
*
* @private
* @returns {void}
*/
_onRefresh() {
const { onRefresh } = this.props;
if (typeof onRefresh === 'function') {
onRefresh();
}
}
_renderItem: Object => Object;
/**
* Renders a single item in the list.
*
* @param {Object} listItem - The item to render.
* @param {string} key - The item needed for rendering using map on web.
* @private
* @returns {Component}
*/
_renderItem(listItem, key: string = '') {
const { item } = listItem;
const { url } = item;
// XXX The value of title cannot be undefined; otherwise, react-native
// will throw a TypeError: Cannot read property of undefined. While it's
// difficult to get an undefined title and very likely requires the
// execution of incorrect source code, it is undesirable to break the
// whole app because of an undefined string.
if (typeof item.title === 'undefined') {
return null;
}
return (
<NavigateSectionListItem
item = { item }
key = { key }
onPress = { this._onPress(url) } />
);
}
_renderListEmptyComponent: () => Object;
/**
* Renders a component to display when the list is empty.
*
* @param {Object} section - The section being rendered.
* @private
* @returns {React$Node}
*/
_renderListEmptyComponent() {
const { onRefresh } = this.props;
if (typeof onRefresh === 'function') {
return (
<NavigateSectionListEmptyComponent />
);
}
return null;
}
_renderSectionHeader: Object => Object;
/**
* Renders a section header.
*
* @param {Object} section - The section being rendered.
* @private
* @returns {React$Node}
*/
_renderSectionHeader(section) {
return (
<NavigateSectionListSectionHeader
section = { section } />
);
}
}
export default NavigateSectionList;

View File

@@ -1 +1,2 @@
export * from './_';
export { default as NavigateSectionList } from './NavigateSectionList';

View File

@@ -34,6 +34,7 @@ export default class Container extends AbstractContainer {
accessible,
onClick,
touchFeedback = onClick,
underlayColor,
visible = true,
...props
} = this.props;
@@ -62,7 +63,8 @@ export default class Container extends AbstractContainer {
{
accessibilityLabel,
accessible,
onPress: onClick
onPress: onClick,
underlayColor
},
element);
}

View File

@@ -1,346 +0,0 @@
// @flow
import React, { Component } from 'react';
import {
SafeAreaView,
SectionList,
Text,
TouchableHighlight,
View
} from 'react-native';
import { Icon } from '../../../font-icons';
import { translate } from '../../../i18n';
import styles, { UNDERLAY_COLOR } from './styles';
type Props = {
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The translate function.
*/
t: Function,
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
onPress: Function,
/**
* Function to be invoked when pull-to-refresh is performed.
*/
onRefresh: Function,
/**
* Function to override the rendered default empty list component.
*/
renderListEmptyComponent: Function,
/**
* Sections to be rendered in the following format:
*
* [
* {
* title: string, <- section title
* key: string, <- unique key for the section
* data: [ <- Array of items in the section
* {
* colorBase: string, <- the color base of the avatar
* title: string, <- item title
* url: string, <- item url
* lines: Array<string> <- additional lines to be rendered
* }
* ]
* }
* ]
*/
sections: Array<Object>
};
/**
* Implements a general section list to display items that have a URL property
* and navigates to (probably) meetings, such as the recent list or the meeting
* list components.
*/
class NavigateSectionList extends Component<Props> {
/**
* Creates an empty section object.
*
* @param {string} title - The title of the section.
* @param {string} key - The key of the section. It must be unique.
* @private
* @returns {Object}
*/
static createSection(title, key) {
return {
data: [],
key,
title
};
}
/**
* Constructor of the NavigateSectionList component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._getAvatarColor = this._getAvatarColor.bind(this);
this._getItemKey = this._getItemKey.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._renderItem = this._renderItem.bind(this);
this._renderItemLine = this._renderItemLine.bind(this);
this._renderItemLines = this._renderItemLines.bind(this);
this._renderListEmptyComponent
= this._renderListEmptyComponent.bind(this);
this._renderSection = this._renderSection.bind(this);
}
/**
* Implements React's Component.render.
* Note: we don't use the refreshing value yet, because refreshing of these
* lists is super quick, no need to complicate the code - yet.
*
* @inheritdoc
*/
render() {
const {
renderListEmptyComponent = this._renderListEmptyComponent,
sections
} = this.props;
return (
<SafeAreaView
style = { styles.container } >
<SectionList
ListEmptyComponent = { renderListEmptyComponent }
keyExtractor = { this._getItemKey }
onRefresh = { this._onRefresh }
refreshing = { false }
renderItem = { this._renderItem }
renderSectionHeader = { this._renderSection }
sections = { sections }
style = { styles.list } />
</SafeAreaView>
);
}
_getAvatarColor: string => Object;
/**
* Returns a style (color) based on the string that determines the color of
* the avatar.
*
* @param {string} colorBase - The string that is the base of the color.
* @private
* @returns {Object}
*/
_getAvatarColor(colorBase) {
if (!colorBase) {
return null;
}
let nameHash = 0;
for (let i = 0; i < colorBase.length; i++) {
nameHash += colorBase.codePointAt(i);
}
return styles[`avatarColor${(nameHash % 5) + 1}`];
}
_getItemKey: (Object, number) => string;
/**
* Generates a unique id to every item.
*
* @param {Object} item - The item.
* @param {number} index - The item index.
* @private
* @returns {string}
*/
_getItemKey(item, index) {
return `${index}-${item.key}`;
}
_onPress: string => Function;
/**
* Returns a function that is used in the onPress callback of the items.
*
* @param {string} url - The URL of the item to navigate to.
* @private
* @returns {Function}
*/
_onPress(url) {
return () => {
const { disabled, onPress } = this.props;
!disabled && url && typeof onPress === 'function' && onPress(url);
};
}
_onRefresh: () => void;
/**
* Invokes the onRefresh callback if present.
*
* @private
* @returns {void}
*/
_onRefresh() {
const { onRefresh } = this.props;
if (typeof onRefresh === 'function') {
onRefresh();
}
}
_renderItem: Object => Object;
/**
* Renders a single item in the list.
*
* @param {Object} listItem - The item to render.
* @private
* @returns {Component}
*/
_renderItem(listItem) {
const { item: { colorBase, lines, title, url } } = listItem;
// XXX The value of title cannot be undefined; otherwise, react-native
// will throw a TypeError: Cannot read property of undefined. While it's
// difficult to get an undefined title and very likely requires the
// execution of incorrect source code, it is undesirable to break the
// whole app because of an undefined string.
if (typeof title === 'undefined') {
return null;
}
return (
<TouchableHighlight
onPress = { this._onPress(url) }
underlayColor = { UNDERLAY_COLOR }>
<View style = { styles.listItem }>
<View style = { styles.avatarContainer } >
<View
style = { [
styles.avatar,
this._getAvatarColor(colorBase)
] } >
<Text style = { styles.avatarContent }>
{ title.substr(0, 1).toUpperCase() }
</Text>
</View>
</View>
<View style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = { [
styles.listItemText,
styles.listItemTitle
] }>
{ title }
</Text>
{ this._renderItemLines(lines) }
</View>
</View>
</TouchableHighlight>
);
}
_renderItemLine: (string, number) => React$Node;
/**
* Renders a single line from the additional lines.
*
* @param {string} line - The line text.
* @param {number} index - The index of the line.
* @private
* @returns {React$Node}
*/
_renderItemLine(line, index) {
if (!line) {
return null;
}
return (
<Text
key = { index }
numberOfLines = { 1 }
style = { styles.listItemText }>
{ line }
</Text>
);
}
_renderItemLines: Array<string> => Array<React$Node>;
/**
* Renders the additional item lines, if any.
*
* @param {Array<string>} lines - The lines to render.
* @private
* @returns {Array<React$Node>}
*/
_renderItemLines(lines) {
return lines && lines.length ? lines.map(this._renderItemLine) : null;
}
_renderListEmptyComponent: () => Object;
/**
* Renders a component to display when the list is empty.
*
* @param {Object} section - The section being rendered.
* @private
* @returns {React$Node}
*/
_renderListEmptyComponent() {
const { t, onRefresh } = this.props;
if (typeof onRefresh === 'function') {
return (
<View style = { styles.pullToRefresh }>
<Text style = { styles.pullToRefreshText }>
{ t('sectionList.pullToRefresh') }
</Text>
<Icon
name = 'menu-down'
style = { styles.pullToRefreshIcon } />
</View>
);
}
return null;
}
_renderSection: Object => Object;
/**
* Renders a section title.
*
* @param {Object} section - The section being rendered.
* @private
* @returns {React$Node}
*/
_renderSection(section) {
return (
<View style = { styles.listSection }>
<Text style = { styles.listSectionText }>
{ section.section.title }
</Text>
</View>
);
}
}
export default translate(NavigateSectionList);

View File

@@ -0,0 +1,48 @@
// @flow
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { Icon } from '../../../font-icons';
import { translate } from '../../../i18n';
import styles from './styles';
type Props = {
/**
* The translate function.
*/
t: Function,
};
/**
* Implements a React Native {@link Component} that is to be displayed when the
* list is empty
*
* @extends Component
*/
class NavigateSectionListEmptyComponent extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return (
<View style = { styles.pullToRefresh }>
<Text style = { styles.pullToRefreshText }>
{ t('sectionList.pullToRefresh') }
</Text>
<Icon
name = 'menu-down'
style = { styles.pullToRefreshIcon } />
</View>
);
}
}
export default translate(NavigateSectionListEmptyComponent);

View File

@@ -0,0 +1,141 @@
// @flow
import React, { Component } from 'react';
import Container from './Container';
import Text from './Text';
import styles, { UNDERLAY_COLOR } from './styles';
import type { Item } from '../../Types';
type Props = {
/**
* item containing data to be rendered
*/
item: Item,
/**
* Function to be invoked when an Item is pressed. The Item's URL is passed.
*/
onPress: Function
}
/**
* Implements a React/Native {@link Component} that renders the Navigate Section
* List Item
*
* @extends Component
*/
export default class NavigateSectionListItem extends Component<Props> {
/**
* Constructor of the NavigateSectionList component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._getAvatarColor = this._getAvatarColor.bind(this);
this._renderItemLine = this._renderItemLine.bind(this);
this._renderItemLines = this._renderItemLines.bind(this);
}
_getAvatarColor: string => Object;
/**
* Returns a style (color) based on the string that determines the color of
* the avatar.
*
* @param {string} colorBase - The string that is the base of the color.
* @private
* @returns {Object}
*/
_getAvatarColor(colorBase) {
if (!colorBase) {
return null;
}
let nameHash = 0;
for (let i = 0; i < colorBase.length; i++) {
nameHash += colorBase.codePointAt(i);
}
return styles[`avatarColor${(nameHash % 5) + 1}`];
}
_renderItemLine: (string, number) => React$Node;
/**
* Renders a single line from the additional lines.
*
* @param {string} line - The line text.
* @param {number} index - The index of the line.
* @private
* @returns {React$Node}
*/
_renderItemLine(line, index) {
if (!line) {
return null;
}
return (
<Text
key = { index }
numberOfLines = { 1 }
style = { styles.listItemText }>
{line}
</Text>
);
}
_renderItemLines: Array<string> => Array<React$Node>;
/**
* Renders the additional item lines, if any.
*
* @param {Array<string>} lines - The lines to render.
* @private
* @returns {Array<React$Node>}
*/
_renderItemLines(lines) {
return lines && lines.length ? lines.map(this._renderItemLine) : null;
}
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
render() {
const { colorBase, lines, title } = this.props.item;
const avatarStyles = {
...styles.avatar,
...this._getAvatarColor(colorBase)
};
return (
<Container
onClick = { this.props.onPress }
style = { styles.listItem }
underlayColor = { UNDERLAY_COLOR }>
<Container style = { styles.avatarContainer }>
<Container style = { avatarStyles }>
<Text style = { styles.avatarContent }>
{title.substr(0, 1).toUpperCase()}
</Text>
</Container>
</Container>
<Container style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = {{
...styles.listItemText,
...styles.listItemTitle
}}>
{title}
</Text>
{this._renderItemLines(lines)}
</Container>
</Container>
);
}
}

View File

@@ -0,0 +1,41 @@
// @flow
import React, { Component } from 'react';
import Container from './Container';
import styles from './styles';
import Text from './Text';
import type { SetionListSection } from '../../Types';
type Props = {
/**
* A section containing the data to be rendered
*/
section: SetionListSection
}
/**
* Implements a React/Native {@link Component} that renders the section header
* of the list
*
* @extends Component
*/
export default class NavigateSectionListSectionHeader extends Component<Props> {
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
render() {
const { section } = this.props.section;
return (
<Container style = { styles.listSection }>
<Text style = { styles.listSectionText }>
{ section.title }
</Text>
</Container>
);
}
}

View File

@@ -0,0 +1,91 @@
// @flow
import React, { Component } from 'react';
import {
SafeAreaView,
SectionList as ReactNativeSectionList
} from 'react-native';
import styles from './styles';
import type { Section } from '../../Types';
/**
* The type of the React {@code Component} props of {@link SectionList}
*/
type Props = {
/**
* Rendered when the list is empty. Can be a React Component Class, a render
* function, or a rendered element.
*/
ListEmptyComponent: Object,
/**
*
* Used to extract a unique key for a given item at the specified index.
* Key is used for caching and as the react key to track item re-ordering.
*/
keyExtractor: Function,
/**
*
* Functions that defines what happens when the list is pulled for refresh
*/
onRefresh: Function,
/**
*
* A boolean that is set true while waiting for new data from a refresh.
*/
refreshing: ?boolean,
/**
*
* Default renderer for every item in every section.
*/
renderItem: Function,
/**
*
* A component rendered at the top of each section. These stick to the top
* of the ScrollView by default on iOS.
*/
renderSectionHeader: Object,
/**
* An array of sections
*/
sections: Array<Section>
};
/**
* Implements a React Native {@link Component} that wraps the React Native
* SectionList component in a SafeAreaView so that it renders the sectionlist
* within the safe area of the device
*
* @extends Component
*/
export default class SectionList extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<SafeAreaView
style = { styles.container } >
<ReactNativeSectionList
ListEmptyComponent = { this.props.ListEmptyComponent }
keyExtractor = { this.props.keyExtractor }
onRefresh = { this.props.onRefresh }
refreshing = { this.props.refreshing }
renderItem = { this.props.renderItem }
renderSectionHeader = { this.props.renderSectionHeader }
sections = { this.props.sections }
style = { styles.list } />
</SafeAreaView>
);
}
}

View File

@@ -1,10 +1,16 @@
export { default as Container } from './Container';
export { default as Header } from './Header';
export { default as NavigateSectionList } from './NavigateSectionList';
export { default as Link } from './Link';
export { default as LoadingIndicator } from './LoadingIndicator';
export { default as NavigateSectionListEmptyComponent } from
'./NavigateSectionListEmptyComponent';
export { default as NavigateSectionListItem }
from './NavigateSectionListItem';
export { default as NavigateSectionListSectionHeader }
from './NavigateSectionListSectionHeader';
export { default as PagedList } from './PagedList';
export { default as Pressable } from './Pressable';
export { default as SideBar } from './SideBar';
export { default as Text } from './Text';
export { default as SectionList } from './SectionList';
export { default as TintedView } from './TintedView';

View File

@@ -0,0 +1,74 @@
// @flow
import React, { Component } from 'react';
import Container from './Container';
import Text from './Text';
import type { Item } from '../../Types';
type Props = {
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
onPress: Function,
/**
* A item containing data to be rendered
*/
item: Item
}
/**
* Implements a React/Web {@link Component} for displaying an item in a
* NavigateSectionList
*
* @extends Component
*/
export default class NavigateSectionListItem extends Component<Props> {
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
render() {
const { lines, title } = this.props.item;
const { onPress } = this.props;
/**
* Initiliazes the date and duration of the conference to the an empty
* string in case for some reason there is an error where the item data
* lines doesn't contain one or both of those values (even though this
* unlikely the app shouldn't break because of it)
* @type {string}
*/
let date = '';
let duration = '';
if (lines[0]) {
date = lines[0];
}
if (lines[1]) {
duration = lines[1];
}
return (
<Container
className = 'navigate-section-list-tile'
onClick = { onPress }>
<Text
className = 'navigate-section-tile-title'>
{ title }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ date }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ duration }
</Text>
</Container>
);
}
}

View File

@@ -0,0 +1,35 @@
// @flow
import React, { Component } from 'react';
import Text from './Text';
import type { Section } from '../../Types';
type Props = {
/**
* A section containing the data to be rendered
*/
section: Section
}
/**
* Implements a React/Web {@link Component} that renders the section header of
* the list
*
* @extends Component
*/
export default class NavigateSectionListSectionHeader extends Component<Props> {
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
render() {
return (
<Text className = 'navigate-section-section-header'>
{ this.props.section.title }
</Text>
);
}
}

View File

@@ -0,0 +1,92 @@
// @flow
import React, { Component } from 'react';
import Container from './Container';
import type { Section } from '../../Types';
type Props = {
/**
* Used to extract a unique key for a given item at the specified index.
* Key is used for caching and as the react key to track item re-ordering.
*/
keyExtractor: Function,
/**
* Returns a React component that renders each Item in the list
*/
renderItem: Function,
/**
* Returns a React component that renders the header for every section
*/
renderSectionHeader: Function,
/**
* An array of sections
*/
sections: Array<Section>,
/**
* defines what happens when an item in the section list is clicked
*/
onItemClick: Function
};
/**
* Implements a React/Web {@link Component} for displaying a list with
* sections similar to React Native's {@code SectionList} in order to
* faciliate cross-platform source code.
*
* @extends Component
*/
export default class SectionList extends Component<Props> {
/**
* Renders the content of this component.
*
* @returns {React.ReactNode}
*/
render() {
const {
renderSectionHeader,
renderItem,
sections,
keyExtractor
} = this.props;
/**
* If there are no recent items we dont want to display anything
*/
if (sections) {
return (
/* eslint-disable no-extra-parens */
<Container
className = 'navigate-section-list'>
{
sections.map((section, sectionIndex) => (
<Container
key = { sectionIndex }>
{ renderSectionHeader(section) }
{ section.data
.map((item, listIndex) => {
const listItem = {
item
};
return renderItem(listItem,
keyExtractor(section,
listIndex));
}) }
</Container>
)
)
}
</Container>
/* eslint-enable no-extra-parens */
);
}
return null;
}
}

View File

@@ -1,4 +1,11 @@
export { default as Container } from './Container';
export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete';
export { default as NavigateSectionListEmptyComponent } from
'./NavigateSectionListEmptyComponent';
export { default as NavigateSectionListItem } from
'./NavigateSectionListItem';
export { default as NavigateSectionListSectionHeader }
from './NavigateSectionListSectionHeader';
export { default as SectionList } from './SectionList';
export { default as Text } from './Text';
export { default as Watermarks } from './Watermarks';

View File

@@ -1,2 +1,3 @@
export * from './components';
export { default as Platform } from './Platform';
export * from './Types';

View File

@@ -0,0 +1,14 @@
/**
* FIXME.
*
* {
* type: SET_SESSION,
* session: {
* url: {string},
* state: {string},
* ...data
* }
* }
* @public
*/
export const SET_SESSION = Symbol('SET_SESSION');

View File

@@ -0,0 +1,16 @@
import { SET_SESSION } from './actionTypes';
/**
* FIXME.
*
* @param {string} session - FIXME.
* @returns {{
* type: SET_SESSION
* }}
*/
export function setSession(session) {
return {
type: SET_SESSION,
session
};
}

View File

@@ -0,0 +1,12 @@
export const SESSION_CONFIGURED = Symbol('SESSION_CONFIGURED');
export const SESSION_ENDED = Symbol('SESSION_ENDED');
export const SESSION_FAILED = Symbol('SESSION_FAILED');
export const SESSION_STARTED = Symbol('SESSION_STARTED');
export const SESSION_WILL_END = Symbol('SESSION_WILL_END');
export const SESSION_WILL_START = Symbol('SESSION_WILL_START');

View File

@@ -0,0 +1,36 @@
// @flow
import { toState } from '../redux';
import { toURLString } from '../util';
/**
* FIXME.
*
* @param {Function|Object} stateful - FIXME.
* @param {string} url - FIXME.
* @returns {*}
*/
export function getSession(stateful: Function | Object, url: string): ?Object {
const state = toState(stateful);
const session = state['features/base/session'].get(url);
if (!session) {
console.info(`SESSION NOT FOUND FOR URL: ${url}`);
}
return session;
}
/**
* FIXME.
*
* @param {Function | Object} stateful - FIXME.
* @returns {Object}
*/
export function getCurrentSession(stateful: Function | Object): ?Object {
const state = toState(stateful);
const { locationURL } = state['features/base/config'];
return getSession(state, toURLString(locationURL));
}

View File

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

View File

@@ -0,0 +1,349 @@
// @flow
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
JITSI_CONFERENCE_URL_KEY,
isRoomValid
} from '../../base/conference';
import {
CONNECTION_DISCONNECTED,
CONNECTION_FAILED,
CONNECTION_WILL_CONNECT
} from '../../base/connection';
import {
MiddlewareRegistry,
toState
} from '../../base/redux';
import { parseURIString, toURLString } from '../../base/util';
import {
SESSION_CONFIGURED,
SESSION_ENDED,
SESSION_FAILED,
SESSION_STARTED,
SESSION_WILL_END,
SESSION_WILL_START
} from './constants';
import { setSession } from './actions';
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from '../config';
import { getCurrentSession, getSession } from './functions';
/**
* Middleware that maintains conference sessions. The features spans across
* three features strictly related to the conference lifecycle.
* The first one is the base/config which configures the session. It's
* 'locationURL' state is used to tell what's the current conference URL the app
* is working with. The session starts as soon as {@link CONFIG_WILL_LOAD} event
* arrives. The {@code locationURL} instance is stored in the session to
* associate the load config request with the session and be able to distinguish
* between the current and outdated load config request failures. After the
* config is loaded the lifecycle moves to the base/connection feature which
* creates a {@code JitsiConnection} and tries to connect to the server. On
* {@code CONNECTION_WILL_CONNECT} the connection instance is stored in the
* session and used later to filter the events similar to what's done for
* the load config requests. The base/conference feature adds the last part to
* the session's lifecycle. A {@code JitsiConference} instance is stored in the
* session on the {@code CONFERENCE_WILL_JOIN} action. A session is considered
* alive as long as either connection or conference is available and
* operational.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
const { type } = action;
switch (type) {
case CONFERENCE_WILL_JOIN: {
const { conference } = action;
const { locationURL } = store.getState()['features/base/connection'];
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session) {
store.dispatch(setSession({
url: session.url,
conference
}));
} else {
console.info(`IGNORED WILL_JOIN FOR: ${url}`);
}
break;
}
case CONFERENCE_JOINED: {
const { conference } = action;
const session = findSessionForConference(store, conference);
const state = session && session.state;
if (state === SESSION_CONFIGURED) {
store.dispatch(
setSession({
// Flow complains that the session can be undefined, but it
// can't if the state is defined.
// $FlowExpectedError
url: session.url,
state: SESSION_STARTED
}));
} else {
// eslint-disable-next-line max-len
console.info(`IGNORED CONF JOINED FOR: ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`);
}
break;
}
case CONFERENCE_LEFT:
case CONFERENCE_FAILED: {
const { conference, error } = action;
const session = findSessionForConference(store, conference);
// FIXME update comments
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may
// be able to eventually join the conference. For example, the app will
// ask the user for a password upon
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
// conference afterwards. Such errors are to not reach the native
// counterpart of the External API (or at least not in the
// fatality/finality semantics attributed to
// conferenceFailed:/onConferenceFailed).
if (session) {
if (!error || isGameOver(store, session, error)) {
if (session.connection) {
store.dispatch(
setSession({
url: session.url,
conference: undefined
}));
} else {
store.dispatch(
setSession({
url: session.url,
state: error ? SESSION_FAILED : SESSION_ENDED,
error
}));
}
}
} else {
// eslint-disable-next-line max-len
console.info(`IGNORED FAILED/LEFT for ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`, error);
}
break;
}
// NOTE WILL_JOIN is fired on SET_ROOM
// case CONFERENCE_WILL_JOIN:
case CONFERENCE_WILL_LEAVE: {
const { conference } = action;
const url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
const session = findSessionForConference(store, conference);
const state = session && session.state;
if (state && state !== SESSION_WILL_END) {
store.dispatch(
setSession({
// Flow complains that the session can be undefined, but it
// can't if the state is defined.
// $FlowExpectedError
url: session.url,
state: SESSION_WILL_END
}));
} else {
console.info(`IGNORED WILL LEAVE FOR ${url}`);
}
break;
}
case CONNECTION_WILL_CONNECT: {
const { connection } = action;
const { locationURL } = store.getState()['features/base/connection'];
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session) {
store.dispatch(
setSession({
url: session.url,
connection,
conference: undefined // Detach from the old conference
}));
} else {
console.info(`IGNORED CONNECTION_WILL_CONNECT FOR: ${url}`);
}
break;
}
case CONNECTION_DISCONNECTED:
case CONNECTION_FAILED: {
const { connection, error } = action;
const session = findSessionForConnection(store, connection);
if (session) {
// Remove connection from the session, but wait for
// the conference to be removed as well.
if (!error || isGameOver(store, session, error)) {
if (session.conference) {
store.dispatch(
setSession({
url: session.url,
connection: undefined
}));
} else {
store.dispatch(
setSession({
url: session.url,
state: error ? SESSION_FAILED : SESSION_ENDED,
error
}));
}
}
} else {
console.info('Ignored DISCONNECTED/FAILED for connection');
}
break;
}
case SET_CONFIG: {
// XXX SET_CONFIG IS ALWAYS RELEVANT
const { locationURL } = store.getState()['features/base/config'];
const url = toURLString(locationURL);
const session = getSession(store, url);
const state = session && session.state;
if (state === SESSION_WILL_START) {
store.dispatch(
setSession({
url,
state: SESSION_CONFIGURED
}));
}
break;
}
case CONFIG_WILL_LOAD: {
const { locationURL } = action;
const url = toURLString(locationURL);
const session = getSession(store, url);
// The back and forth to string conversion is here, because there's no
// guarantee that the locationURL will be the exact custom structure
// which contains the room property.
let { room } = parseURIString(url);
// Validate the room
room = isRoomValid(room) ? room : undefined;
if (room && !session) {
store.dispatch(
setSession({
url,
state: SESSION_WILL_START,
locationURL,
room
}));
} else if (room && session) {
// Update to the new locationURL instance
store.dispatch(
setSession({
url,
locationURL
}));
} else {
console.info(`IGNORED CFG WILL LOAD FOR ${url}`);
}
break;
}
case LOAD_CONFIG_ERROR: {
const { error, locationURL } = action;
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session && session.locationURL === locationURL) {
if (isGameOver(store, session, error)) {
store.dispatch(
setSession({
url,
state: SESSION_FAILED,
error
}));
}
} else {
console.info(`IGNORED LOAD_CONFIG_ERROR FOR: ${url}`);
}
break;
}
}
return result;
});
/**
* FIXME A session is to be terminated either when the recoverable flag is
* explicitly set to {@code false} or if the error arrives for a session which
* is no longer current (the app has started working with another session).
* This can happen when a conference which is being disconnected fails in which
* case the session needs to be ended even if the flag is not {@code false}
* because we know that there's no fatal error handling. This is kind of
* a contract between the fatal error feature and the session which probably
* indicates that the fatal error detection and handling should be incorporated
* into the session feature.
*
* @param {Object | Function} stateful - FIXME.
* @param {Object} session - FIXME.
* @param {Object} error - FIXME.
* @returns {boolean}
*/
function isGameOver(stateful, session, error) {
return getCurrentSession(stateful) !== session
|| error.recoverable === false;
}
/**
* FIXME.
*
* @param {Object | Function} stateful - FIXME.
* @param {JitsiConnection} connection - FIXME.
* @returns {Object|undefined}
*/
function findSessionForConnection(stateful, connection) {
const state = toState(stateful);
for (const session of state['features/base/session'].values()) {
if (session.connection === connection) {
return session;
}
}
console.info('Session not found for a connection');
return undefined;
}
/**
* FIXME.
*
* @param {Object | Function} stateful - FIXME.
* @param {JitsiConference} conference - FIXME.
* @returns {Object|undefined}
*/
function findSessionForConference(stateful, conference) {
const state = toState(stateful);
for (const session of state['features/base/session'].values()) {
if (session.conference === conference) {
return session;
}
}
console.info('Session not found for a conference');
return undefined;
}

View File

@@ -0,0 +1,71 @@
// @flow
import { assign, ReducerRegistry } from '../../base/redux';
import { getSymbolDescription } from '../util';
import { SET_SESSION } from './actionTypes';
import {
SESSION_FAILED,
SESSION_ENDED,
SESSION_WILL_START
} from './constants';
ReducerRegistry.register('features/base/session',
(state = new Map(), action) => {
switch (action.type) {
case SET_SESSION:
return _setSession(state, action);
}
return state;
});
/**
* FIXME.
*
* @param {Object} featureState - FIXME.
* @param {Object} action - FIXME.
* @returns {Map<any, any>} - FIXME.
* @private
*/
function _setSession(featureState, action) {
const { url, state, ...data } = action.session;
const session = featureState.get(url);
const nextState = new Map(featureState);
// Drop the whole action if the url is not defined
if (!url) {
console.error('SET SESSION - NO URL');
return nextState;
}
if (session) {
if (state === SESSION_ENDED || state === SESSION_FAILED) {
nextState.delete(url);
} else {
nextState.set(
url,
assign(session, {
url,
state: state ? state : session.state,
...data
}));
}
} else if (state === SESSION_WILL_START) {
nextState.set(
url, {
url,
state,
...data
});
}
console.info(
'SESSION STATE REDUCED: ',
new Map(nextState),
url,
state && getSymbolDescription(state),
action.session.error);
return nextState;
}

View File

@@ -138,7 +138,7 @@ function _initSettings(featureState) {
if (settings.audioOutputDeviceId
!== JitsiMeetJS.mediaDevices.getAudioOutputDevice()) {
JitsiMeetJS.mediaDevices.setAudioOutputDevice(
audioOutputDeviceId
settings.audioOutputDeviceId
).catch(ex => {
logger.warn('Failed to set audio output device from local '
+ 'storage. Default audio output device will be used'

View File

@@ -20,6 +20,15 @@ const throttledPersistState
state => PersistenceRegistry.persistState(state),
PERSIST_STATE_DELAY);
// Web only code.
// We need the <tt>if</tt> beacuse it appears that on mobile the polyfill is not
// executed yet.
if (typeof window.addEventListener === 'function') {
window.addEventListener('unload', () => {
throttledPersistState.flush();
});
}
/**
* A master MiddleWare to selectively persist state. Please use the
* {@link persisterconfig.json} to set which subtrees of the redux state should

View File

@@ -233,7 +233,7 @@ export function setTrackMuted(track, muted) {
// Track might be already disposed so ignore such an error.
if (error.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
// FIXME Emit mute failed, so that the app can show error dialog.
console.error(`set track ${f} failed`, error);
logger.error(`set track ${f} failed`, error);
}
});
}

View File

@@ -1,5 +1,7 @@
// @flow
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The app linking scheme.
* TODO: This should be read from the manifest files later.
@@ -174,7 +176,7 @@ function _objectToURLParamsArray(obj = {}) {
params.push(
`${key}=${encodeURIComponent(JSON.stringify(obj[key]))}`);
} catch (e) {
console.warn(`Error encoding ${key}: ${e}`);
logger.warn(`Error encoding ${key}: ${e}`);
}
}

View File

@@ -4,18 +4,22 @@ import {
Transport
} from '../../../modules/transport';
import { createDeviceChangedEvent, sendAnalytics } from '../analytics';
import {
getAudioOutputDeviceId,
setAudioInputDevice,
setAudioOutputDevice,
setAudioOutputDeviceId,
setVideoInputDevice
} from '../base/devices';
import { i18next } from '../base/i18n';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { updateSettings } from '../base/settings';
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
import { getDeviceSelectionDialogProps } from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Opens a popup window with the device selection dialog in it.
*
@@ -115,23 +119,22 @@ function _processRequest(dispatch, getState, request, responseCallback) { // esl
responseCallback(getState()['features/base/devices']);
break;
case 'setDevice': {
let action;
const { device } = request;
switch (device.kind) {
case 'audioinput':
action = setAudioInputDevice;
dispatch(setAudioInputDevice(device.id));
break;
case 'audiooutput':
action = setAudioOutputDevice;
setAudioOutputDeviceId(device.id, dispatch);
break;
case 'videoinput':
action = setVideoInputDevice;
dispatch(setVideoInputDevice(device.id));
break;
default:
}
dispatch(action(device.id));
responseCallback(true);
break;
}
@@ -179,6 +182,10 @@ export function submitDeviceSelectionTab(newState) {
if (newState.selectedVideoInputId
&& newState.selectedVideoInputId
!== currentState.selectedVideoInputId) {
dispatch(updateSettings({
cameraDeviceId: newState.selectedVideoInputId
}));
dispatch(
setVideoInputDevice(newState.selectedVideoInputId));
}
@@ -186,6 +193,10 @@ export function submitDeviceSelectionTab(newState) {
if (newState.selectedAudioInputId
&& newState.selectedAudioInputId
!== currentState.selectedAudioInputId) {
dispatch(updateSettings({
micDeviceId: newState.selectedAudioInputId
}));
dispatch(
setAudioInputDevice(newState.selectedAudioInputId));
}
@@ -193,8 +204,19 @@ export function submitDeviceSelectionTab(newState) {
if (newState.selectedAudioOutputId
&& newState.selectedAudioOutputId
!== currentState.selectedAudioOutputId) {
dispatch(
setAudioOutputDevice(newState.selectedAudioOutputId));
sendAnalytics(createDeviceChangedEvent('audio', 'output'));
setAudioOutputDeviceId(
newState.selectedAudioOutputId,
dispatch)
.then(() => logger.log('changed audio output device'))
.catch(err => {
logger.warn(
'Failed to change audio output device.',
'Default or previously set audio output device will',
' be used instead.',
err);
});
}
};
}

View File

@@ -12,6 +12,8 @@ import AudioOutputPreview from './AudioOutputPreview';
import DeviceSelector from './DeviceSelector';
import VideoInputPreview from './VideoInputPreview';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The type of the React {@code Component} props of {@link DeviceSelection}.
*/
@@ -64,6 +66,12 @@ export type Props = {
*/
hideAudioOutputSelect: boolean,
/**
* An optional callback to invoke after the component has completed its
* mount logic.
*/
mountCallback?: Function,
/**
* The id of the audio input device to preview.
*/
@@ -134,8 +142,12 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
* @inheritdoc
*/
componentDidMount() {
this._createAudioInputTrack(this.props.selectedAudioInputId);
this._createVideoInputTrack(this.props.selectedVideoInputId);
Promise.all([
this._createAudioInputTrack(this.props.selectedAudioInputId),
this._createVideoInputTrack(this.props.selectedVideoInputId)
])
.catch(err => logger.warn('Failed to initialize preview tracks', err))
.then(() => this.props.mountCallback && this.props.mountCallback());
}
/**
@@ -212,7 +224,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
* @returns {void}
*/
_createAudioInputTrack(deviceId) {
this._disposeAudioInputPreview()
return this._disposeAudioInputPreview()
.then(() => createLocalTrack('audio', deviceId))
.then(jitsiLocalTrack => {
this.setState({
@@ -234,7 +246,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
* @returns {void}
*/
_createVideoInputTrack(deviceId) {
this._disposeVideoInputPreview()
return this._disposeVideoInputPreview()
.then(() => createLocalTrack('video', deviceId))
.then(jitsiLocalTrack => {
if (!jitsiLocalTrack) {

View File

@@ -0,0 +1,19 @@
/**
* The type of Redux action which changes Google API state.
*
* {
* type: SET_GOOGLE_API_STATE
* }
* @public
*/
export const SET_GOOGLE_API_STATE = Symbol('SET_GOOGLE_API_STATE');
/**
* The type of Redux action which changes Google API profile state.
*
* {
* type: SET_GOOGLE_API_PROFILE
* }
* @public
*/
export const SET_GOOGLE_API_PROFILE = Symbol('SET_GOOGLE_API_PROFILE');

View File

@@ -0,0 +1,139 @@
/* @flow */
import {
SET_GOOGLE_API_PROFILE,
SET_GOOGLE_API_STATE
} from './actionTypes';
import { GOOGLE_API_STATES } from './constants';
import googleApi from './googleApi';
/**
* Loads Google API.
*
* @param {string} clientId - The client ID to be used with the API library.
* @returns {Function}
*/
export function loadGoogleAPI(clientId: string) {
return (dispatch: Dispatch<*>) =>
googleApi.get()
.then(() => googleApi.initializeClient(clientId))
.then(() => dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.LOADED }))
.then(() => googleApi.isSignedIn())
.then(isSignedIn => {
if (isSignedIn) {
dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.SIGNED_IN });
}
});
}
/**
* Prompts the participant to sign in to the Google API Client Library.
*
* @returns {function(Dispatch<*>): Promise<string | never>}
*/
export function signIn() {
return (dispatch: Dispatch<*>) => googleApi.get()
.then(() => googleApi.signInIfNotSignedIn())
.then(() => dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
}));
}
/**
* Updates the profile data that is currently used.
*
* @returns {function(Dispatch<*>): Promise<string | never>}
*/
export function updateProfile() {
return (dispatch: Dispatch<*>) => googleApi.get()
.then(() => googleApi.signInIfNotSignedIn())
.then(() => dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
}))
.then(() => googleApi.getCurrentUserProfile())
.then(profile => dispatch({
type: SET_GOOGLE_API_PROFILE,
profileEmail: profile.getEmail()
}));
}
/**
* Executes a request for a list of all YouTube broadcasts associated with
* user currently signed in to the Google API Client Library.
*
* @returns {function(): (Promise<*>|Promise<any[] | never>)}
*/
export function requestAvailableYouTubeBroadcasts() {
return () =>
googleApi.requestAvailableYouTubeBroadcasts()
.then(response => {
// Takes in a list of broadcasts from the YouTube API,
// removes dupes, removes broadcasts that cannot get a stream key,
// and parses the broadcasts into flat objects.
const broadcasts = response.result.items;
const parsedBroadcasts = {};
for (let i = 0; i < broadcasts.length; i++) {
const broadcast = broadcasts[i];
const boundStreamID = broadcast.contentDetails.boundStreamId;
if (boundStreamID && !parsedBroadcasts[boundStreamID]) {
parsedBroadcasts[boundStreamID] = {
boundStreamID,
id: broadcast.id,
status: broadcast.status.lifeCycleStatus,
title: broadcast.snippet.title
};
}
}
return Object.values(parsedBroadcasts);
});
}
/**
* Fetches the stream key for a YouTube broadcast and updates the internal
* state to display the associated stream key as being entered.
*
* @param {string} boundStreamID - The bound stream ID associated with the
* broadcast from which to get the stream key.
* @returns {function(): (Promise<*>|Promise<{
* streamKey: (*|string),
* selectedBoundStreamID: *} | never>)}
*/
export function requestLiveStreamsForYouTubeBroadcast(boundStreamID: string) {
return () =>
googleApi.requestLiveStreamsForYouTubeBroadcast(boundStreamID)
.then(response => {
const broadcasts = response.result.items;
const streamName = broadcasts
&& broadcasts[0]
&& broadcasts[0].cdn.ingestionInfo.streamName;
const streamKey = streamName || '';
return {
streamKey,
selectedBoundStreamID: boundStreamID
};
});
}
/**
* Forces the Google web client application to prompt for a sign in, such as
* when changing account, and will then fetch available YouTube broadcasts.
*
* @returns {function(): (Promise<*>|Promise<{
* streamKey: (*|string),
* selectedBoundStreamID: *} | never>)}
*/
export function showAccountSelection() {
return () =>
googleApi.showAccountSelection();
}

View File

@@ -0,0 +1,33 @@
// @flow
/**
* The Google API scopes to request access to for streaming.
*
* @type {Array<string>}
*/
export const GOOGLE_API_SCOPES = [
'https://www.googleapis.com/auth/youtube.readonly'
];
/**
* An enumeration of the different states the Google API can be in.
*
* @private
* @type {Object}
*/
export const GOOGLE_API_STATES = {
/**
* The state in which the Google API still needs to be loaded.
*/
NEEDS_LOADING: 0,
/**
* The state in which the Google API is loaded and ready for use.
*/
LOADED: 1,
/**
* The state in which a user has been logged in through the Google API.
*/
SIGNED_IN: 2
};

View File

@@ -0,0 +1,5 @@
export { GOOGLE_API_STATES } from './constants';
export * from './googleApi';
export * from './actions';
import './reducer';

View File

@@ -0,0 +1,40 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import {
SET_GOOGLE_API_PROFILE,
SET_GOOGLE_API_STATE
} from './actionTypes';
import { GOOGLE_API_STATES } from './constants';
/**
* The default state is the Google API needs loading.
*
* @type {{googleAPIState: number}}
*/
const DEFAULT_STATE = {
googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
profileEmail: ''
};
/**
* Reduces the Redux actions of the feature features/google-api.
*/
ReducerRegistry.register('features/google-api',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_GOOGLE_API_STATE:
return {
...state,
googleAPIState: action.googleAPIState
};
case SET_GOOGLE_API_PROFILE:
return {
...state,
profileEmail: action.profileEmail
};
}
return state;
});

View File

@@ -1,4 +1,4 @@
/* @flow */
// @flow
import { NativeModules } from 'react-native';
@@ -11,6 +11,9 @@ import {
} from '../../base/conference';
import { MiddlewareRegistry } from '../../base/redux';
const { AudioMode } = NativeModules;
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Middleware that captures conference actions and sets the correct audio mode
* based on the type of conference. Audio-only conferences don't use the speaker
@@ -20,7 +23,7 @@ import { MiddlewareRegistry } from '../../base/redux';
* @returns {Function}
*/
MiddlewareRegistry.register(({ getState }) => next => action => {
const AudioMode = NativeModules.AudioMode;
const result = next(action);
if (AudioMode) {
let mode;
@@ -43,13 +46,13 @@ MiddlewareRegistry.register(({ getState }) => next => action => {
*/
case CONFERENCE_JOINED:
case SET_AUDIO_ONLY: {
if (getState()['features/base/conference'].conference
|| action.conference) {
mode
= action.audioOnly
? AudioMode.AUDIO_CALL
: AudioMode.VIDEO_CALL;
}
const { audioOnly, conference }
= getState()['features/base/conference'];
conference
&& (mode = audioOnly
? AudioMode.AUDIO_CALL
: AudioMode.VIDEO_CALL);
break;
}
}
@@ -57,10 +60,10 @@ MiddlewareRegistry.register(({ getState }) => next => action => {
if (typeof mode !== 'undefined') {
AudioMode.setMode(mode)
.catch(err =>
console.error(
logger.error(
`Failed to set audio mode ${String(mode)}: ${err}`));
}
}
return next(action);
return result;
});

View File

@@ -5,14 +5,7 @@ import uuid from 'uuid';
import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
import { appNavigate, getName } from '../../app';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import {
CONFERENCE_FAILED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_JOINED,
SET_AUDIO_ONLY,
getCurrentConference
} from '../../base/conference';
import { SET_AUDIO_ONLY } from '../../base/conference';
import { getInviteURL } from '../../base/connection';
import {
MEDIA_TYPE,
@@ -20,6 +13,16 @@ import {
setAudioMuted
} from '../../base/media';
import { MiddlewareRegistry } from '../../base/redux';
import {
SESSION_CONFIGURED,
SESSION_ENDED,
SESSION_FAILED,
SESSION_STARTED,
SET_SESSION,
getCurrentSession,
getSession,
setSession
} from '../../base/session';
import {
TRACK_ADDED,
TRACK_REMOVED,
@@ -51,21 +54,12 @@ CallKit && MiddlewareRegistry.register(store => next => action => {
});
break;
case CONFERENCE_FAILED:
return _conferenceFailed(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case CONFERENCE_LEFT:
return _conferenceLeft(store, next, action);
case CONFERENCE_WILL_JOIN:
return _conferenceWillJoin(store, next, action);
case SET_AUDIO_ONLY:
return _setAudioOnly(store, next, action);
case SET_SESSION:
return _setSession(store, next, action);
case TRACK_ADDED:
case TRACK_REMOVED:
case TRACK_UPDATED:
@@ -127,6 +121,35 @@ function _appWillMount({ dispatch, getState }, next, action) {
return result;
}
/**
* FIXME.
*
* @param {Store}store - FIXME.
* @param {Dispatch} next - FIXME.
* @param {Action} action - FIXME.
* @returns {*} The value returned by {@code next(action)}.
* @private
*/
function _setSession(store, next, action) {
const { state } = action.session;
switch (state) {
case SESSION_CONFIGURED:
return _sessionConfigured(store, next, action);
case SESSION_ENDED:
return _sessionEnded(store, next, action);
case SESSION_FAILED:
return _sessionFailed(store, next, action);
case SESSION_STARTED:
return _sessionJoined(store, next, action);
}
return next(action);
}
/**
* Notifies the feature callkit that the action {@link CONFERENCE_FAILED} is
* being dispatched within a specific redux {@code store}.
@@ -140,21 +163,14 @@ function _appWillMount({ dispatch, getState }, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceFailed(store, next, action) {
const result = next(action);
function _sessionFailed(store, next, action) {
const callUUID = _getCallUUIDForSessionAction(store, action);
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may be
// able to eventually join the conference.
if (!action.error.recoverable) {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.reportCallFailed(callUUID);
}
if (callUUID) {
CallKit.reportCallFailed(callUUID);
}
return result;
return next(action);
}
/**
@@ -170,16 +186,14 @@ function _conferenceFailed(store, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceJoined(store, next, action) {
const result = next(action);
const { callUUID } = action.conference;
function _sessionJoined(store, next, action) {
const callUUID = _getCallUUIDForSessionAction(store, action);
if (callUUID) {
CallKit.reportConnectedOutgoingCall(callUUID);
}
return result;
return next(action);
}
/**
@@ -195,16 +209,34 @@ function _conferenceJoined(store, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceLeft(store, next, action) {
const result = next(action);
const { callUUID } = action.conference;
function _sessionEnded(store, next, action) {
const callUUID = _getCallUUIDForSessionAction(store, action);
if (callUUID) {
CallKit.endCall(callUUID);
}
return result;
return next(action);
}
/**
* FIXME.
*
* @param {Store} store - FIXME.
* @param {Object} action - FIXME.
* @returns {string|undefined}
* @private
*/
function _getCallUUIDForSessionAction(store, action) {
const url = action.session.url;
const session = getSession(store, url);
const callUUID = session && session.callkit && session.callkit.callUUID;
if (!callUUID) {
console.info(`CALLKIT SESSION NOT FOUND FOR URL: ${url}`);
}
return callUUID;
}
/**
@@ -220,27 +252,32 @@ function _conferenceLeft(store, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceWillJoin({ getState }, next, action) {
const result = next(action);
const { conference } = action;
function _sessionConfigured({ getState }, next, action) {
const state = getState();
const { callHandle, callUUID } = state['features/base/config'];
const { callHandle, callUUID: _callUUID } = state['features/base/config'];
const url = getInviteURL(state);
const handle = callHandle || url.toString();
const hasVideo = !isVideoMutedByAudioOnly(state);
// When assigning the call UUID, do so in upper case, since iOS will return
// it upper cased.
conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
const callUUID = (_callUUID || uuid.v4()).toUpperCase();
CallKit.startCall(conference.callUUID, handle, hasVideo)
// Store the callUUID in the session
action.session.callkit = {
callUUID
};
CallKit.startCall(callUUID, handle, hasVideo)
.then(() => {
const session = getSession(getState(), action.session.url);
const { callee } = state['features/base/jwt'];
const displayName
= state['features/base/config'].callDisplayName
|| (callee && callee.name)
|| state['features/base/conference'].room;
|| (session && session.room);
console.info(`CALLKIT WILL USE NAME: ${displayName}`);
const muted
= isLocalTrackMuted(
@@ -248,11 +285,11 @@ function _conferenceWillJoin({ getState }, next, action) {
MEDIA_TYPE.AUDIO);
// eslint-disable-next-line object-property-newline
CallKit.updateCall(conference.callUUID, { displayName, hasVideo });
CallKit.setMuted(conference.callUUID, muted);
CallKit.updateCall(callUUID, { displayName, hasVideo });
CallKit.setMuted(callUUID, muted);
});
return result;
return next(action);
}
/**
@@ -263,18 +300,47 @@ function _conferenceWillJoin({ getState }, next, action) {
* @returns {void}
*/
function _onPerformEndCallAction({ callUUID }) {
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
const conference = getCurrentConference(getState);
const { dispatch } = this; // eslint-disable-line no-invalid-this
// eslint-disable-next-line max-len
const session = _findSessionForCallUUID(this, callUUID); // eslint-disable-line no-invalid-this
if (conference && conference.callUUID === callUUID) {
if (session) {
// We arrive here when a call is ended by the system, for example, when
// another incoming call is received and the user selects "End &
// Accept".
delete conference.callUUID;
dispatch(
setSession({
url: session.url,
callkit: undefined
}));
dispatch(appNavigate(undefined));
}
}
/**
* FIXME.
*
* @param {Store} getState - FIXME.
* @param {string} callUUID - FIXME.
* @returns {Object|null}
* @private
*/
function _findSessionForCallUUID({ getState }, callUUID) {
const sessions = getState()['features/base/session'];
for (const session of sessions.values()) {
const _callUUID = session.callkit && session.callkit.callUUID;
if (callUUID === _callUUID) {
return session;
}
}
console.info(`SESSION NOT FOUND FOR CALL ID: ${callUUID}`);
return null;
}
/**
* Handles CallKit's event {@code performSetMutedCallAction}.
*
@@ -283,10 +349,11 @@ function _onPerformEndCallAction({ callUUID }) {
* @returns {void}
*/
function _onPerformSetMutedCallAction({ callUUID, muted }) {
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
const conference = getCurrentConference(getState);
const { dispatch } = this; // eslint-disable-line no-invalid-this
// eslint-disable-next-line max-len
const session = _findSessionForCallUUID(this, callUUID); // eslint-disable-line no-invalid-this
if (conference && conference.callUUID === callUUID) {
if (session) {
muted = Boolean(muted); // eslint-disable-line no-param-reassign
sendAnalytics(createTrackMutedEvent('audio', 'callkit', muted));
dispatch(setAudioMuted(muted, /* ensureTrack */ true));
@@ -316,11 +383,11 @@ function _onPerformSetMutedCallAction({ callUUID, muted }) {
function _setAudioOnly({ getState }, next, action) {
const result = next(action);
const state = getState();
const conference = getCurrentConference(state);
const session = getCurrentSession(state);
if (conference && conference.callUUID) {
if (session && session.callUUID) {
CallKit.updateCall(
conference.callUUID,
session.callUUID,
{ hasVideo: !action.audioOnly });
}
@@ -369,20 +436,25 @@ function _syncTrackState({ getState }, next, action) {
const result = next(action);
const { jitsiTrack } = action.track;
const state = getState();
const conference = getCurrentConference(state);
if (jitsiTrack.isLocal() && conference && conference.callUUID) {
// It could go over all sessions here, but even if we'd support simultaneous
// sessions / putting on hold, probably only the active session would be
// holding the tracks.
const session = getCurrentSession(state);
const callUUID = session && session.callkit && session.callkit.callUUID;
if (jitsiTrack.isLocal() && callUUID) {
switch (jitsiTrack.getType()) {
case 'audio': {
const tracks = state['features/base/tracks'];
const muted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
CallKit.setMuted(conference.callUUID, muted);
CallKit.setMuted(callUUID, muted);
break;
}
case 'video': {
CallKit.updateCall(
conference.callUUID,
callUUID,
{ hasVideo: !isVideoMutedByAudioOnly(state) });
break;
}

View File

@@ -1,22 +1,56 @@
// @flow
import { NativeModules } from 'react-native';
import { getAppProp } from '../../app';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
JITSI_CONFERENCE_URL_KEY,
SET_ROOM,
forEachConference,
isRoomValid
CONFERENCE_WILL_LEAVE
} from '../../base/conference';
import { LOAD_CONFIG_ERROR } from '../../base/config';
import { CONNECTION_FAILED } from '../../base/connection';
import { MiddlewareRegistry } from '../../base/redux';
import { getSymbolDescription, toURLString } from '../../base/util';
import {
SESSION_ENDED,
SESSION_FAILED,
SESSION_STARTED,
SESSION_WILL_END,
SESSION_WILL_START,
SET_SESSION
} from '../../base/session';
import { getSymbolDescription } from '../../base/util';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
/**
* FIXME.
*
* @param {Symbol} state - FIXME.
* @returns {string}
* @private
*/
function _stateToApiEventName(state) {
switch (state) {
case SESSION_WILL_START:
return getSymbolDescription(CONFERENCE_WILL_JOIN);
case SESSION_STARTED:
return getSymbolDescription(CONFERENCE_JOINED);
case SESSION_WILL_END:
return getSymbolDescription(CONFERENCE_WILL_LEAVE);
case SESSION_ENDED:
return getSymbolDescription(CONFERENCE_LEFT);
case SESSION_FAILED:
return getSymbolDescription(CONFERENCE_FAILED);
default:
return undefined;
}
}
import { sendEvent } from './functions';
/**
@@ -27,63 +61,19 @@ import { sendEvent } from './functions';
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
action.type && console.info(`ACTION ${getSymbolDescription(action.type)}`);
const result = next(action);
const { type } = action;
switch (type) {
case CONFERENCE_FAILED: {
const { error, ...data } = action;
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may
// be able to eventually join the conference. For example, the app will
// ask the user for a password upon
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
// conference afterwards. Such errors are to not reach the native
// counterpart of the External API (or at least not in the
// fatality/finality semantics attributed to
// conferenceFailed:/onConferenceFailed).
if (!error.recoverable) {
_sendConferenceEvent(store, /* action */ {
error: _toErrorString(error),
...data
});
}
break;
}
case CONFERENCE_JOINED:
case CONFERENCE_LEFT:
case CONFERENCE_WILL_JOIN:
case CONFERENCE_WILL_LEAVE:
_sendConferenceEvent(store, action);
break;
case CONNECTION_FAILED:
!action.error.recoverable
&& _sendConferenceFailedOnConnectionError(store, action);
case SET_SESSION:
_setSession(store, action);
break;
case ENTER_PICTURE_IN_PICTURE:
sendEvent(store, getSymbolDescription(type), /* data */ {});
break;
case LOAD_CONFIG_ERROR: {
const { error, locationURL } = action;
sendEvent(
store,
getSymbolDescription(type),
/* data */ {
error: _toErrorString(error),
url: toURLString(locationURL)
});
break;
}
case SET_ROOM:
_maybeTriggerEarlyConferenceWillJoin(store, action);
break;
}
return result;
@@ -110,145 +100,47 @@ function _toErrorString(
}
/**
* If {@link SET_ROOM} action happens for a valid conference room this method
* will emit an early {@link CONFERENCE_WILL_JOIN} event to let the external API
* know that a conference is being joined. Before that happens a connection must
* be created and only then base/conference feature would emit
* {@link CONFERENCE_WILL_JOIN}. That is fine for the Jitsi Meet app, because
* that's the a conference instance gets created, but it's too late for
* the external API to learn that. The latter {@link CONFERENCE_WILL_JOIN} is
* swallowed in {@link _swallowEvent}.
*
* @param {Store} store - The redux store.
* @param {Action} action - The redux action.
* @returns {void}
*/
function _maybeTriggerEarlyConferenceWillJoin(store, action) {
const { locationURL } = store.getState()['features/base/connection'];
const { room } = action;
isRoomValid(room) && locationURL && sendEvent(
store,
getSymbolDescription(CONFERENCE_WILL_JOIN),
/* data */ {
url: toURLString(locationURL)
});
}
/**
* Sends an event to the native counterpart of the External API for a specific
* conference-related redux action.
*
* @param {Store} store - The redux store.
* @param {Action} action - The redux action.
* @returns {void}
*/
function _sendConferenceEvent(
store: Object,
action: {
conference: Object,
type: Symbol,
url: ?string
}) {
const { conference, type, ...data } = action;
// For these (redux) actions, conference identifies a JitsiConference
// instance. The external API cannot transport such an object so we have to
// transport an "equivalent".
if (conference) {
data.url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
}
_swallowEvent(store, action, data)
|| sendEvent(store, getSymbolDescription(type), data);
}
/**
* Sends {@link CONFERENCE_FAILED} event when the {@link CONNECTION_FAILED}
* occurs. It should be done only if the connection fails before the conference
* instance is created. Otherwise the eventual failure event is supposed to be
* emitted by the base/conference feature.
*
* @param {Store} store - The redux store.
* @param {Action} action - The redux action.
* @returns {void}
*/
function _sendConferenceFailedOnConnectionError(store, action) {
const { locationURL } = store.getState()['features/base/connection'];
const { connection } = action;
locationURL
&& forEachConference(
store,
// If there's any conference in the base/conference state then the
// base/conference feature is supposed to emit a failure.
conference => conference.getConnection() !== connection)
&& sendEvent(
store,
getSymbolDescription(CONFERENCE_FAILED),
/* data */ {
url: toURLString(locationURL),
error: action.error.name
});
}
/**
* Determines whether to not send a {@code CONFERENCE_LEFT} event to the native
* counterpart of the External API.
* Sends a specific event to the native counterpart of the External API. Native
* apps may listen to such events via the mechanisms provided by the (native)
* mobile Jitsi Meet SDK.
*
* @param {Object} store - The redux store.
* @param {Action} action - The redux action which is causing the sending of the
* event.
* @param {string} name - The name of the event to send.
* @param {Object} data - The details/specifics of the event to send determined
* by/associated with the specified {@code action}.
* @returns {boolean} If the specified event is to not be sent, {@code true};
* otherwise, {@code false}.
* by/associated with the specified {@code name}.
* @private
* @returns {void}
*/
function _swallowConferenceLeft({ getState }, action, { url }) {
// XXX Internally, we work with JitsiConference instances. Externally
// though, we deal with URL strings. The relation between the two is many to
// one so it's technically and practically possible (by externally loading
// the same URL string multiple times) to try to send CONFERENCE_LEFT
// externally for a URL string which identifies a JitsiConference that the
// app is internally legitimately working with.
let swallowConferenceLeft = false;
function _sendEvent(store: Object, name: string, data: Object) {
// The JavaScript App needs to provide uniquely identifying information to
// the native ExternalAPI module so that the latter may match the former to
// the native JitsiMeetView which hosts it.
const externalAPIScope = getAppProp(store, 'externalAPIScope');
url
&& forEachConference(getState, (conference, conferenceURL) => {
if (conferenceURL && conferenceURL.toString() === url) {
swallowConferenceLeft = true;
}
console.info(
`EXT EVENT ${name} URL: ${data.url} DATA: ${JSON.stringify(data)}`);
return !swallowConferenceLeft;
});
return swallowConferenceLeft;
externalAPIScope
&& NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
}
/**
* Determines whether to not send a specific event to the native counterpart of
* the External API.
* FIXME.
*
* @param {Object} store - The redux store.
* @param {Action} action - The redux action which is causing the sending of the
* event.
* @param {Object} data - The details/specifics of the event to send determined
* by/associated with the specified {@code action}.
* @returns {boolean} If the specified event is to not be sent, {@code true};
* otherwise, {@code false}.
* @param {Store} store - FIXME.
* @param {Action} action - FIXME.
* @returns {void}
* @private
*/
function _swallowEvent(store, action, data) {
switch (action.type) {
case CONFERENCE_LEFT:
return _swallowConferenceLeft(store, action, data);
case CONFERENCE_WILL_JOIN:
// CONFERENCE_WILL_JOIN is dispatched to the external API on SET_ROOM,
// before the connection is created, so we need to swallow the original
// one emitted by base/conference.
return true;
function _setSession(store, action) {
const { error, state, url } = action.session;
const apiEventName = _stateToApiEventName(state);
default:
return false;
}
apiEventName && _sendEvent(
store,
apiEventName,
/* data */ {
url,
error: error && _toErrorString(error)
});
}

View File

@@ -1,31 +0,0 @@
import { ImageCache } from './';
/**
* Notifies about the successful download of an {@code Image} source. The name
* is inspired by {@code Image}. The downloaded {@code Image} source is not
* available because (1) I do not know how to get it from {@link ImageCache} and
* (2) we do not need it bellow. The function was explicitly introduced to cut
* down on unnecessary {@code ImageCache} {@code observer} instances.
*
* @private
* @returns {void}
*/
function _onLoad() {
// ImageCache requires an observer; otherwise, we do not need it because we
// merely want to initiate the download and do not care what happens with it
// afterwards.
}
/**
* Initiates the retrieval of a specific {@code Image} source (if it has not
* been initiated already). Due to limitations of {@link ImageCache}, the source
* may have at most one {@code uri}. The name is inspired by {@code Image}.
*
* @param {Object} source - The {@code Image} source with preferably exactly
* one {@code uri}.
* @public
* @returns {void}
*/
export function prefetch(source) {
ImageCache && ImageCache.get().on(source, _onLoad, /* immutable */ true);
}

View File

@@ -1,4 +0,0 @@
export * from './functions';
export * from './react-native-img-cache';
import './middleware';

View File

@@ -1,91 +0,0 @@
/* @flow */
import { APP_WILL_MOUNT } from '../../base/app';
import {
getAvatarURL,
getLocalParticipant,
getParticipantById,
PARTICIPANT_ID_CHANGED,
PARTICIPANT_JOINED,
PARTICIPANT_UPDATED
} from '../../base/participants';
import { MiddlewareRegistry } from '../../base/redux';
import { ImageCache, prefetch } from './';
/**
* The indicator which determines whether avatar URLs are to be prefetched in
* the middleware here. Unless/until the implementation starts observing the
* redux store instead of the respective redux actions, the value should very
* likely be {@code false} because the middleware here is pretty much the last
* to get a chance to figure out that an avatar URL may be used. Besides, it is
* somewhat uninformed to download just about anything that may eventually be
* used or not.
*
* @private
* @type {boolean}
*/
const _PREFETCH_AVATAR_URLS = false;
/**
* Middleware which captures app startup and conference actions in order to
* clear the image cache.
*
* @returns {Function}
*/
MiddlewareRegistry.register(({ getState }) => next => action => {
switch (action.type) {
case APP_WILL_MOUNT:
// XXX CONFERENCE_FAILED/LEFT are no longer used here because they
// are tricky to get right as detectors of the moments in time at which
// CachedImage is not used. Anyway, if ImageCache is to be cleared from
// time to time, SET_LOCATION_URL is a much easier detector of such
// opportune times. Fixes at least one 100%-reproducible case of
// "TypeError: Cannot read property handlers of undefined." Anyway, in
// order to reduce the re-downloading of the same avatars, eventually we
// decided to not clear during the runtime of the app (other that at the
// beginning that is).
ImageCache && ImageCache.get().clear();
break;
case PARTICIPANT_ID_CHANGED:
case PARTICIPANT_JOINED:
case PARTICIPANT_UPDATED: {
if (!_PREFETCH_AVATAR_URLS) {
break;
}
const result = next(action);
// Initiate the downloads of participants' avatars as soon as possible.
// 1. Figure out the participant (instance).
let { participant } = action;
if (participant) {
if (participant.id) {
participant = getParticipantById(getState, participant.id);
} else if (participant.local) {
participant = getLocalParticipant(getState);
} else {
participant = undefined;
}
} else if (action.oldValue && action.newValue) {
participant = getParticipantById(getState, action.newValue);
}
if (participant) {
// 2. Get the participant's avatar URL.
const uri = getAvatarURL(participant);
if (uri) {
// 3. Initiate the download of the participant's avatar.
prefetch({ uri });
}
}
return result;
}
}
return next(action);
});

View File

@@ -1 +0,0 @@
export * from './react-native-img-cache.yes';

View File

@@ -1 +0,0 @@
export * from './react-native-img-cache.yes';

View File

@@ -1,16 +0,0 @@
// XXX The third-party react-native modules react-native-fetch-blob utilizes the
// same HTTP library as react-native i.e. okhttp. Unfortunately, that means that
// the versions of okhttp on which react-native and react-native-fetch-blob
// depend may have incompatible APIs. Such an incompatibility will be made
// apparent at compile time and the developer doing the compilation may choose
// to not compile react-native-fetch-blob's source code.
// XXX The choice between the use of react-native-img-cache could've been done
// at runtime based on whether NativeModules.RNFetchBlob is defined if only
// react-native-fetch-blob would've completely protected itself. At the time of
// this writing its source code appears to be attempting to protect itself from
// missing native binaries but that protection is incomplete and there's a
// TypeError.
import { Image } from 'react-native';
export { Image as CachedImage, undefined as ImageCache };

View File

@@ -1 +0,0 @@
export { CachedImage, ImageCache } from 'react-native-img-cache';

View File

@@ -7,6 +7,8 @@ import { Platform } from '../../base/react';
import { ENTER_PICTURE_IN_PICTURE } from './actionTypes';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Enters (or rather initiates entering) picture-in-picture.
* Helper function to enter PiP mode. This is triggered by user request
@@ -34,7 +36,7 @@ export function enterPictureInPicture() {
p.then(
() => dispatch({ type: ENTER_PICTURE_IN_PICTURE }),
e => console.warn(`Error entering PiP mode: ${e}`));
e => logger.warn(`Error entering PiP mode: ${e}`));
}
};
}

View File

@@ -1,3 +1,9 @@
import {
SESSION_FAILED,
getCurrentSession,
setSession
} from '../base/session';
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR,
@@ -57,3 +63,37 @@ export function setFatalError(fatalError) {
fatalError
};
}
/**
* FIXME naming is not quite accurate - came from the previous method which was
* reemitting the action. I feel that this part needs more discussion. Changing
* it back to emitting the original action which caused the fatal error will
* also require changes to how it's being detected (currently through the state
* listener, but we'd have to go back to the middleware way).
*
* @returns {Function}
*/
export function reemitFatalError() {
return (dispatch, getState) => {
const state = getState();
const { fatalError } = state['features/overlay'];
if (fatalError) {
const session = getCurrentSession(state);
if (session) {
dispatch(
setSession({
url: session.url,
state: SESSION_FAILED,
error: fatalError
}));
} else {
console.info('No current session!');
}
dispatch(setFatalError(undefined));
} else {
console.info('NO FATAL ERROR');
}
};
}

View File

@@ -8,7 +8,7 @@ import { LoadingIndicator } from '../../base/react';
import AbstractPageReloadOverlay, { abstractMapStateToProps }
from './AbstractPageReloadOverlay';
import { setFatalError } from '../actions';
import { reemitFatalError } from '../actions';
import OverlayFrame from './OverlayFrame';
import { pageReloadOverlay as styles } from './styles';
@@ -41,7 +41,7 @@ class PageReloadOverlay extends AbstractPageReloadOverlay {
*/
_onCancel() {
clearInterval(this._interval);
this.props.dispatch(setFatalError(undefined));
this.props.dispatch(reemitFatalError());
this.props.dispatch(appNavigate(undefined));
}

View File

@@ -0,0 +1,112 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { appNavigate, getDefaultURL } from '../../app';
import { translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import type { Section } from '../../base/react';
import { isRecentListEnabled, toDisplayableList } from '../functions';
/**
* The type of the React {@code Component} props of {@link RecentList}
*/
type Props = {
/**
* Renders the list disabled.
*/
disabled: boolean,
/**
* The redux store's {@code dispatch} function.
*/
dispatch: Dispatch<*>,
/**
* The translate function.
*/
t: Function,
/**
* The default server URL.
*/
_defaultServerURL: string,
/**
* The recent list from the Redux store.
*/
_recentList: Array<Section>
};
/**
* The cross platform container rendering the list of the recently joined rooms.
*
*/
class RecentList extends Component<Props> {
/**
* Initializes a new {@code RecentList} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onPress = this._onPress.bind(this);
}
/**
* Implements the React Components's render method.
*
* @inheritdoc
*/
render() {
if (!isRecentListEnabled()) {
return null;
}
const { disabled, t, _defaultServerURL, _recentList } = this.props;
const recentList = toDisplayableList(_recentList, t, _defaultServerURL);
return (
<NavigateSectionList
disabled = { disabled }
onPress = { this._onPress }
sections = { recentList } />
);
}
_onPress: string => Function;
/**
* Handles the list's navigate action.
*
* @private
* @param {string} url - The url string to navigate to.
* @returns {void}
*/
_onPress(url) {
const { dispatch } = this.props;
dispatch(appNavigate(url));
}
}
/**
* Maps redux state to component props.
*
* @param {Object} state - The redux state.
* @returns {{
* _defaultServerURL: string,
* _recentList: Array
* }}
*/
export function _mapStateToProps(state: Object) {
return {
_defaultServerURL: getDefaultURL(state),
_recentList: state['features/recent-list']
};
}
export default translate(connect(_mapStateToProps)(RecentList));

View File

@@ -1,234 +0,0 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { appNavigate, getDefaultURL } from '../../app';
import {
getLocalizedDateFormatter,
getLocalizedDurationFormatter,
translate
} from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import { parseURIString } from '../../base/util';
/**
* The type of the React {@code Component} props of {@link RecentList}
*/
type Props = {
/**
* Renders the list disabled.
*/
disabled: boolean,
/**
* The redux store's {@code dispatch} function.
*/
dispatch: Dispatch<*>,
/**
* The translate function.
*/
t: Function,
/**
* The default server URL.
*/
_defaultServerURL: string,
/**
* The recent list from the Redux store.
*/
_recentList: Array<Object>
};
/**
* The native container rendering the list of the recently joined rooms.
*
*/
class RecentList extends Component<Props> {
/**
* Initializes a new {@code RecentList} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onPress = this._onPress.bind(this);
this._toDateString = this._toDateString.bind(this);
this._toDurationString = this._toDurationString.bind(this);
this._toDisplayableItem = this._toDisplayableItem.bind(this);
this._toDisplayableList = this._toDisplayableList.bind(this);
}
/**
* Implements the React Components's render method.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
return (
<NavigateSectionList
disabled = { disabled }
onPress = { this._onPress }
sections = { this._toDisplayableList() } />
);
}
_onPress: string => Function;
/**
* Handles the list's navigate action.
*
* @private
* @param {string} url - The url string to navigate to.
* @returns {void}
*/
_onPress(url) {
const { dispatch } = this.props;
dispatch(appNavigate(url));
}
_toDisplayableItem: Object => Object;
/**
* Creates a displayable list item of a recent list entry.
*
* @private
* @param {Object} item - The recent list entry.
* @returns {Object}
*/
_toDisplayableItem(item) {
const { _defaultServerURL } = this.props;
const location = parseURIString(item.conference);
const baseURL = `${location.protocol}//${location.host}`;
const serverName = baseURL === _defaultServerURL ? null : location.host;
return {
colorBase: serverName,
key: `key-${item.conference}-${item.date}`,
lines: [
this._toDateString(item.date),
this._toDurationString(item.duration),
serverName
],
title: location.room,
url: item.conference
};
}
_toDisplayableList: () => Array<Object>;
/**
* Transforms the history list to a displayable list
* with sections.
*
* @private
* @returns {Array<Object>}
*/
_toDisplayableList() {
const { _recentList, t } = this.props;
const { createSection } = NavigateSectionList;
const todaySection = createSection(t('recentList.today'), 'today');
const yesterdaySection
= createSection(t('recentList.yesterday'), 'yesterday');
const earlierSection
= createSection(t('recentList.earlier'), 'earlier');
const today = new Date().toDateString();
const yesterdayDate = new Date();
yesterdayDate.setDate(yesterdayDate.getDate() - 1);
const yesterday = yesterdayDate.toDateString();
for (const item of _recentList) {
const itemDay = new Date(item.date).toDateString();
const displayableItem = this._toDisplayableItem(item);
if (itemDay === today) {
todaySection.data.push(displayableItem);
} else if (itemDay === yesterday) {
yesterdaySection.data.push(displayableItem);
} else {
earlierSection.data.push(displayableItem);
}
}
const displayableList = [];
if (todaySection.data.length) {
todaySection.data.reverse();
displayableList.push(todaySection);
}
if (yesterdaySection.data.length) {
yesterdaySection.data.reverse();
displayableList.push(yesterdaySection);
}
if (earlierSection.data.length) {
earlierSection.data.reverse();
displayableList.push(earlierSection);
}
return displayableList;
}
_toDateString: number => string;
/**
* Generates a date string for the item.
*
* @private
* @param {number} itemDate - The item's timestamp.
* @returns {string}
*/
_toDateString(itemDate) {
const date = new Date(itemDate);
const m = getLocalizedDateFormatter(itemDate);
if (date.toDateString() === new Date().toDateString()) {
// The date is today, we use fromNow format.
return m.fromNow();
}
return m.format('lll');
}
_toDurationString: number => string;
/**
* Generates a duration string for the item.
*
* @private
* @param {number} duration - The item's duration.
* @returns {string}
*/
_toDurationString(duration) {
if (duration) {
return getLocalizedDurationFormatter(duration).humanize();
}
return null;
}
}
/**
* Maps redux state to component props.
*
* @param {Object} state - The redux state.
* @returns {{
* _defaultServerURL: string,
* _recentList: Array
* }}
*/
export function _mapStateToProps(state: Object) {
return {
_defaultServerURL: getDefaultURL(state),
_recentList: state['features/recent-list']
};
}
export default translate(connect(_mapStateToProps)(RecentList));

View File

@@ -0,0 +1,76 @@
import {
getLocalizedDateFormatter,
getLocalizedDurationFormatter
} from '../base/i18n';
import { parseURIString } from '../base/util';
/**
* Creates a displayable list item of a recent list entry.
*
* @private
* @param {Object} item - The recent list entry.
* @param {string} defaultServerURL - The default server URL.
* @param {Function} t - The translate function.
* @returns {Object}
*/
export function toDisplayableItem(item, defaultServerURL, t) {
const location = parseURIString(item.conference);
const baseURL = `${location.protocol}//${location.host}`;
const serverName = baseURL === defaultServerURL ? null : location.host;
return {
colorBase: serverName,
key: `key-${item.conference}-${item.date}`,
lines: [
_toDateString(item.date, t),
_toDurationString(item.duration),
serverName
],
title: location.room,
url: item.conference
};
}
/**
* Generates a duration string for the item.
*
* @private
* @param {number} duration - The item's duration.
* @returns {string}
*/
export function _toDurationString(duration) {
if (duration) {
return getLocalizedDurationFormatter(duration);
}
return null;
}
/**
* Generates a date string for the item.
*
* @private
* @param {number} itemDate - The item's timestamp.
* @param {Function} t - The translate function.
* @returns {string}
*/
export function _toDateString(itemDate, t) {
const m = getLocalizedDateFormatter(itemDate);
const date = new Date(itemDate);
const dateInMs = date.getTime();
const now = new Date();
const todayInMs = (new Date()).setHours(0, 0, 0, 0);
const yesterdayInMs = todayInMs - 86400000; // 1 day = 86400000ms
if (dateInMs >= todayInMs) {
return m.fromNow();
} else if (dateInMs >= yesterdayInMs) {
return t('dateUtils.yesterday');
} else if (date.getFullYear() !== now.getFullYear()) {
// We only want to include the year in the date if its not the current
// year.
return m.format('ddd, MMMM DD h:mm A, gggg');
}
return m.format('ddd, MMMM DD h:mm A');
}

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