mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-22 07:47:50 +00:00
Compare commits
29 Commits
2969
...
base_sessi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2191e3a28 | ||
|
|
72e3e8593d | ||
|
|
67a8b4915d | ||
|
|
468d4a7150 | ||
|
|
2a01e29fec | ||
|
|
90a64d30dc | ||
|
|
31905d4f63 | ||
|
|
7e1d97665a | ||
|
|
b978851a0f | ||
|
|
ef49817eaf | ||
|
|
cac8888b37 | ||
|
|
81853d971a | ||
|
|
b9c5ed3b03 | ||
|
|
0892e0b644 | ||
|
|
b41bf22be7 | ||
|
|
a1cc9bce91 | ||
|
|
8d3cecad86 | ||
|
|
bd8559fad6 | ||
|
|
fb75180632 | ||
|
|
046b06e436 | ||
|
|
af7c69a1aa | ||
|
|
7ad0639f7a | ||
|
|
54a1853e60 | ||
|
|
27021ea271 | ||
|
|
f5a667ad9e | ||
|
|
2b9ce40533 | ||
|
|
d3dd833f21 | ||
|
|
1cc372868b | ||
|
|
a6956c7c34 |
@@ -1,4 +1,4 @@
|
||||
osx_image: xcode9.3
|
||||
osx_image: xcode9.4
|
||||
language: objective-c
|
||||
script:
|
||||
- "./ios/travis-ci/build-ipa.sh"
|
||||
|
||||
6
android/app/proguard-rules.pro
vendored
6
android/app/proguard-rules.pro
vendored
@@ -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.** {*;}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
43
css/_navigate_section_list.scss
Normal file
43
css/_navigate_section_list.scss
Normal 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;
|
||||
}
|
||||
@@ -78,4 +78,5 @@
|
||||
@import 'modals/invite/add-people';
|
||||
@import 'deep-linking/main';
|
||||
@import 'transcription-subtitles';
|
||||
@import 'navigate_section_list';
|
||||
/* Modules END */
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -130,6 +130,7 @@ cp "${CERT_DIR}/dev-profile.mobileprovision" ~/Library/MobileDevice/Provisionin
|
||||
npm install
|
||||
|
||||
cd ios
|
||||
pod update
|
||||
pod install
|
||||
cd ..
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
8
modules/API/external/external_api.js
vendored
8
modules/API/external/external_api.js
vendored
@@ -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
|
||||
|
||||
52
package-lock.json
generated
52
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -19,7 +19,7 @@ class VideoTrack extends AbstractVideoTrack {
|
||||
static propTypes = AbstractVideoTrack.propTypes
|
||||
|
||||
/**
|
||||
* Renders video element with animation.
|
||||
* Renders the video element for the associated video track.
|
||||
*
|
||||
* @override
|
||||
* @returns {ReactElement}
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
@@ -17,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'
|
||||
},
|
||||
|
||||
|
||||
@@ -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,135 +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,
|
||||
|
||||
// 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,
|
||||
style: imageStyle
|
||||
});
|
||||
|
||||
if (viewStyle) {
|
||||
element = React.createElement(View, { style: viewStyle }, element);
|
||||
}
|
||||
|
||||
return element;
|
||||
return (
|
||||
<Fragment>
|
||||
{ source && this._renderAvatar() }
|
||||
{ useDefaultAvatar && this._renderDefaultAvatar() }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
75
react/features/base/react/Types.js
Normal file
75
react/features/base/react/Types.js
Normal 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
|
||||
}
|
||||
223
react/features/base/react/components/NavigateSectionList.js
Normal file
223
react/features/base/react/components/NavigateSectionList.js
Normal 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;
|
||||
@@ -1 +1,2 @@
|
||||
export * from './_';
|
||||
export { default as NavigateSectionList } from './NavigateSectionList';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
91
react/features/base/react/components/native/SectionList.js
Normal file
91
react/features/base/react/components/native/SectionList.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
92
react/features/base/react/components/web/SectionList.js
Normal file
92
react/features/base/react/components/web/SectionList.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './components';
|
||||
export { default as Platform } from './Platform';
|
||||
export * from './Types';
|
||||
|
||||
14
react/features/base/session/actionTypes.js
Normal file
14
react/features/base/session/actionTypes.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* {
|
||||
* type: SET_SESSION,
|
||||
* session: {
|
||||
* url: {string},
|
||||
* state: {string},
|
||||
* ...data
|
||||
* }
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_SESSION = Symbol('SET_SESSION');
|
||||
16
react/features/base/session/actions.js
Normal file
16
react/features/base/session/actions.js
Normal 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
|
||||
};
|
||||
}
|
||||
12
react/features/base/session/constants.js
Normal file
12
react/features/base/session/constants.js
Normal 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');
|
||||
36
react/features/base/session/functions.js
Normal file
36
react/features/base/session/functions.js
Normal 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));
|
||||
}
|
||||
7
react/features/base/session/index.js
Normal file
7
react/features/base/session/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './constants';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
349
react/features/base/session/middleware.js
Normal file
349
react/features/base/session/middleware.js
Normal 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;
|
||||
}
|
||||
71
react/features/base/session/reducer.js
Normal file
71
react/features/base/session/reducer.js
Normal 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;
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
19
react/features/google-api/actionTypes.js
Normal file
19
react/features/google-api/actionTypes.js
Normal 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');
|
||||
139
react/features/google-api/actions.js
Normal file
139
react/features/google-api/actions.js
Normal 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();
|
||||
}
|
||||
33
react/features/google-api/constants.js
Normal file
33
react/features/google-api/constants.js
Normal 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
|
||||
};
|
||||
5
react/features/google-api/index.js
Normal file
5
react/features/google-api/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { GOOGLE_API_STATES } from './constants';
|
||||
export * from './googleApi';
|
||||
export * from './actions';
|
||||
|
||||
import './reducer';
|
||||
40
react/features/google-api/reducer.js
Normal file
40
react/features/google-api/reducer.js
Normal 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;
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './functions';
|
||||
export * from './react-native-img-cache';
|
||||
|
||||
import './middleware';
|
||||
@@ -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);
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from './react-native-img-cache.yes';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './react-native-img-cache.yes';
|
||||
@@ -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 };
|
||||
@@ -1 +0,0 @@
|
||||
export { CachedImage, ImageCache } from 'react-native-img-cache';
|
||||
@@ -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}`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
112
react/features/recent-list/components/RecentList.js
Normal file
112
react/features/recent-list/components/RecentList.js
Normal 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));
|
||||
@@ -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));
|
||||
76
react/features/recent-list/functions.any.js
Normal file
76
react/features/recent-list/functions.any.js
Normal 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');
|
||||
}
|
||||
71
react/features/recent-list/functions.native.js
Normal file
71
react/features/recent-list/functions.native.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { NavigateSectionList } from '../base/react';
|
||||
|
||||
import { toDisplayableItem } from './functions.any';
|
||||
|
||||
/**
|
||||
* Transforms the history list to a displayable list
|
||||
* with sections.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<Object>} recentList - The recent list form the redux store.
|
||||
* @param {Function} t - The translate function.
|
||||
* @param {string} defaultServerURL - The default server URL.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function toDisplayableList(recentList, t, defaultServerURL) {
|
||||
const { createSection } = NavigateSectionList;
|
||||
const todaySection = createSection(t('dateUtils.today'), 'today');
|
||||
const yesterdaySection
|
||||
= createSection(t('dateUtils.yesterday'), 'yesterday');
|
||||
const earlierSection
|
||||
= createSection(t('dateUtils.earlier'), 'earlier');
|
||||
const today = new Date();
|
||||
const todayString = today.toDateString();
|
||||
const yesterday = new Date();
|
||||
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayString = yesterday.toDateString();
|
||||
|
||||
for (const item of recentList) {
|
||||
const itemDateString = new Date(item.date).toDateString();
|
||||
const displayableItem = toDisplayableItem(item, defaultServerURL, t);
|
||||
|
||||
if (itemDateString === todayString) {
|
||||
todaySection.data.push(displayableItem);
|
||||
} else if (itemDateString === yesterdayString) {
|
||||
yesterdaySection.data.push(displayableItem);
|
||||
} else {
|
||||
earlierSection.data.push(displayableItem);
|
||||
}
|
||||
}
|
||||
const displayableList = [];
|
||||
|
||||
// the recent list in the redux store has the latest date in the last index
|
||||
// therefore all the sectionLists' data that was created by parsing through
|
||||
// the recent list is in reverse order and must be reversed for the most
|
||||
// item to show first
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if recent list is enabled and <tt>false</tt> otherwise.
|
||||
*
|
||||
* @returns {boolean} <tt>true</tt> if recent list is enabled and <tt>false</tt>
|
||||
* otherwise.
|
||||
*/
|
||||
export function isRecentListEnabled() {
|
||||
return true;
|
||||
}
|
||||
47
react/features/recent-list/functions.web.js
Normal file
47
react/features/recent-list/functions.web.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/* global interfaceConfig */
|
||||
|
||||
import { NavigateSectionList } from '../base/react';
|
||||
|
||||
import { toDisplayableItem } from './functions.any';
|
||||
|
||||
|
||||
/**
|
||||
* Transforms the history list to a displayable list
|
||||
* with sections.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<Object>} recentList - The recent list form the redux store.
|
||||
* @param {Function} t - The translate function.
|
||||
* @param {string} defaultServerURL - The default server URL.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function toDisplayableList(recentList, t, defaultServerURL) {
|
||||
const { createSection } = NavigateSectionList;
|
||||
const section
|
||||
= createSection(t('recentList.joinPastMeeting'), 'joinPastMeeting');
|
||||
|
||||
// We only want the last three conferences we were in for web.
|
||||
for (const item of recentList.slice(-3)) {
|
||||
const displayableItem = toDisplayableItem(item, defaultServerURL, t);
|
||||
|
||||
section.data.push(displayableItem);
|
||||
}
|
||||
const displayableList = [];
|
||||
|
||||
if (section.data.length) {
|
||||
section.data.reverse();
|
||||
displayableList.push(section);
|
||||
}
|
||||
|
||||
return displayableList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if recent list is enabled and <tt>false</tt> otherwise.
|
||||
*
|
||||
* @returns {boolean} <tt>true</tt> if recent list is enabled and <tt>false</tt>
|
||||
* otherwise.
|
||||
*/
|
||||
export function isRecentListEnabled() {
|
||||
return interfaceConfig.RECENT_LIST_ENABLED;
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
// @flow
|
||||
|
||||
import { APP_WILL_MOUNT } from '../base/app';
|
||||
import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference';
|
||||
import {
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
SET_ROOM,
|
||||
JITSI_CONFERENCE_URL_KEY
|
||||
} from '../base/conference';
|
||||
import { addKnownDomains } from '../base/known-domains';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { parseURIString } from '../base/util';
|
||||
|
||||
import { _storeCurrentConference, _updateConferenceDuration } from './actions';
|
||||
import { isRecentListEnabled } from './functions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Middleware that captures joined rooms so they can be saved into
|
||||
@@ -16,15 +23,17 @@ import { _storeCurrentConference, _updateConferenceDuration } from './actions';
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(store, next, action);
|
||||
if (isRecentListEnabled()) {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(store, next, action);
|
||||
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
return _conferenceWillLeave(store, next, action);
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
return _conferenceWillLeave(store, next, action);
|
||||
|
||||
case SET_ROOM:
|
||||
return _setRoom(store, next, action);
|
||||
case SET_ROOM:
|
||||
return _setRoom(store, next, action);
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
@@ -77,9 +86,27 @@ function _appWillMount({ dispatch, getState }, next, action) {
|
||||
* @returns {*} The result returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceWillLeave({ dispatch, getState }, next, action) {
|
||||
let locationURL;
|
||||
|
||||
/**
|
||||
* FIXME:
|
||||
* It is better to use action.conference[JITSI_CONFERENCE_URL_KEY]
|
||||
* in order to make sure we get the url the conference is leaving
|
||||
* from (i.e. the room we are leaving from) because if the order of events
|
||||
* is different, we cannot be guranteed that the location URL in base
|
||||
* connection is the url we are leaving from... not the one we are going to
|
||||
* (the latter happens on mobile -- if we use the web implementation);
|
||||
* however, the conference object on web does not have
|
||||
* JITSI_CONFERENCE_URL_KEY so we cannot call it and must use the other way
|
||||
*/
|
||||
if (typeof APP === 'undefined') {
|
||||
locationURL = action.conference[JITSI_CONFERENCE_URL_KEY];
|
||||
} else {
|
||||
locationURL = getState()['features/base/connection'].locationURL;
|
||||
}
|
||||
dispatch(
|
||||
_updateConferenceDuration(
|
||||
getState()['features/base/connection'].locationURL));
|
||||
locationURL));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
|
||||
import { APP_WILL_MOUNT } from '../base/app';
|
||||
import { getURLWithoutParamsNormalized } from '../base/connection';
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
_STORE_CURRENT_CONFERENCE,
|
||||
_UPDATE_CONFERENCE_DURATION
|
||||
} from './actionTypes';
|
||||
import { isRecentListEnabled } from './functions';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -50,19 +50,21 @@ PersistenceRegistry.register(STORE_NAME);
|
||||
ReducerRegistry.register(
|
||||
STORE_NAME,
|
||||
(state = _getLegacyRecentRoomList(), action) => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(state);
|
||||
if (isRecentListEnabled()) {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(state);
|
||||
case _STORE_CURRENT_CONFERENCE:
|
||||
return _storeCurrentConference(state, action);
|
||||
|
||||
case _STORE_CURRENT_CONFERENCE:
|
||||
return _storeCurrentConference(state, action);
|
||||
|
||||
case _UPDATE_CONFERENCE_DURATION:
|
||||
return _updateConferenceDuration(state, action);
|
||||
|
||||
default:
|
||||
return state;
|
||||
case _UPDATE_CONFERENCE_DURATION:
|
||||
return _updateConferenceDuration(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,18 @@ export type Props = {
|
||||
*/
|
||||
_googleApiApplicationClientID: string,
|
||||
|
||||
/**
|
||||
* The current state of interactions with the Google API. Determines what
|
||||
* Google related UI should display.
|
||||
*/
|
||||
_googleAPIState: number,
|
||||
|
||||
/**
|
||||
* The email of the user currently logged in to the Google web client
|
||||
* application.
|
||||
*/
|
||||
_googleProfileEmail: string,
|
||||
|
||||
/**
|
||||
* The live stream key that was used before.
|
||||
*/
|
||||
@@ -60,18 +72,6 @@ export type State = {
|
||||
*/
|
||||
errorType: ?string,
|
||||
|
||||
/**
|
||||
* The current state of interactions with the Google API. Determines what
|
||||
* Google related UI should display.
|
||||
*/
|
||||
googleAPIState: number,
|
||||
|
||||
/**
|
||||
* The email of the user currently logged in to the Google web client
|
||||
* application.
|
||||
*/
|
||||
googleProfileEmail: string,
|
||||
|
||||
/**
|
||||
* The boundStreamID of the broadcast currently selected in the broadcast
|
||||
* dropdown.
|
||||
@@ -84,36 +84,6 @@ export type State = {
|
||||
streamKey: string
|
||||
};
|
||||
|
||||
/**
|
||||
* An enumeration of the different states the Google API can be in while
|
||||
* interacting with {@code StartLiveStreamDialog}.
|
||||
*
|
||||
* @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,
|
||||
|
||||
/**
|
||||
* The state in which the Google API encountered an error either loading
|
||||
* or with an API request.
|
||||
*/
|
||||
ERROR: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an abstract class for the StartLiveStreamDialog on both platforms.
|
||||
*
|
||||
@@ -136,8 +106,6 @@ export default class AbstractStartLiveStreamDialog
|
||||
this.state = {
|
||||
broadcasts: undefined,
|
||||
errorType: undefined,
|
||||
googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
|
||||
googleProfileEmail: '',
|
||||
selectedBoundStreamID: undefined,
|
||||
streamKey: ''
|
||||
};
|
||||
@@ -331,6 +299,8 @@ export function _mapStateToProps(state: Object) {
|
||||
_conference: state['features/base/conference'].conference,
|
||||
_googleApiApplicationClientID:
|
||||
state['features/base/config'].googleApiApplicationClientID,
|
||||
_googleAPIState: state['features/google-api'].googleAPIState,
|
||||
_googleProfileEmail: state['features/google-api'].profileEmail,
|
||||
_streamKey: state['features/recording'].streamKey
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,19 +6,24 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import googleApi from '../../googleApi';
|
||||
import {
|
||||
updateProfile,
|
||||
GOOGLE_API_STATES,
|
||||
loadGoogleAPI,
|
||||
requestAvailableYouTubeBroadcasts,
|
||||
requestLiveStreamsForYouTubeBroadcast,
|
||||
showAccountSelection,
|
||||
signIn
|
||||
} from '../../../google-api';
|
||||
|
||||
import AbstractStartLiveStreamDialog, {
|
||||
_mapStateToProps,
|
||||
GOOGLE_API_STATES,
|
||||
type Props
|
||||
} from './AbstractStartLiveStreamDialog';
|
||||
import BroadcastsDropdown from './BroadcastsDropdown';
|
||||
import GoogleSignInButton from './GoogleSignInButton';
|
||||
import StreamKeyForm from './StreamKeyForm';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* A React Component for requesting a YouTube stream key to use for live
|
||||
* streaming of the current conference.
|
||||
@@ -40,6 +45,7 @@ class StartLiveStreamDialog
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onGetYouTubeBroadcasts = this._onGetYouTubeBroadcasts.bind(this);
|
||||
this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
|
||||
this._onGoogleSignIn = this._onGoogleSignIn.bind(this);
|
||||
this._onRequestGoogleSignIn = this._onRequestGoogleSignIn.bind(this);
|
||||
this._onYouTubeBroadcastIDSelected
|
||||
= this._onYouTubeBroadcastIDSelected.bind(this);
|
||||
@@ -58,23 +64,23 @@ class StartLiveStreamDialog
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onInitializeGoogleApi() {
|
||||
return googleApi.get()
|
||||
.then(() => googleApi.initializeClient(
|
||||
this.props._googleApiApplicationClientID))
|
||||
.then(() => this._setStateIfMounted({
|
||||
googleAPIState: GOOGLE_API_STATES.LOADED
|
||||
}))
|
||||
.then(() => googleApi.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (isSignedIn) {
|
||||
return this._onGetYouTubeBroadcasts();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this._setStateIfMounted({
|
||||
googleAPIState: GOOGLE_API_STATES.ERROR
|
||||
});
|
||||
});
|
||||
this.props.dispatch(
|
||||
loadGoogleAPI(this.props._googleApiApplicationClientID))
|
||||
.catch(response => this._parseErrorFromResponse(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically selects the input field's value after starting to edit the
|
||||
* display name.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(previousProps) {
|
||||
if (previousProps._googleAPIState === GOOGLE_API_STATES.LOADED
|
||||
&& this.props._googleAPIState === GOOGLE_API_STATES.SIGNED_IN) {
|
||||
this._onGetYouTubeBroadcasts();
|
||||
}
|
||||
}
|
||||
|
||||
_onGetYouTubeBroadcasts: () => Promise<*>;
|
||||
@@ -84,42 +90,39 @@ class StartLiveStreamDialog
|
||||
* list of the user's YouTube broadcasts.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
* @returns {void}
|
||||
*/
|
||||
_onGetYouTubeBroadcasts() {
|
||||
return googleApi.get()
|
||||
.then(() => googleApi.signInIfNotSignedIn())
|
||||
.then(() => googleApi.getCurrentUserProfile())
|
||||
.then(profile => {
|
||||
this._setStateIfMounted({
|
||||
googleProfileEmail: profile.getEmail(),
|
||||
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
|
||||
});
|
||||
})
|
||||
.then(() => googleApi.requestAvailableYouTubeBroadcasts())
|
||||
.then(response => {
|
||||
const broadcasts = this._parseBroadcasts(response.result.items);
|
||||
this.props.dispatch(updateProfile())
|
||||
.catch(response => this._parseErrorFromResponse(response));
|
||||
|
||||
this.props.dispatch(requestAvailableYouTubeBroadcasts())
|
||||
.then(broadcasts => {
|
||||
this._setStateIfMounted({
|
||||
broadcasts
|
||||
});
|
||||
|
||||
if (broadcasts.length === 1 && !this.state.streamKey) {
|
||||
if (broadcasts.length === 1) {
|
||||
const broadcast = broadcasts[0];
|
||||
|
||||
this._onYouTubeBroadcastIDSelected(broadcast.boundStreamID);
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
// Only show an error if an external request was made with the
|
||||
// Google api. Do not error if the login in canceled.
|
||||
if (response && response.result) {
|
||||
this._setStateIfMounted({
|
||||
errorType: this._parseErrorFromResponse(response),
|
||||
googleAPIState: GOOGLE_API_STATES.ERROR
|
||||
});
|
||||
}
|
||||
});
|
||||
.catch(response => this._parseErrorFromResponse(response));
|
||||
}
|
||||
|
||||
_onGoogleSignIn: () => Object;
|
||||
|
||||
/**
|
||||
* Forces the Google web client application to prompt for a sign in, such as
|
||||
* when changing account, and will then fetch available YouTube broadcasts.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onGoogleSignIn() {
|
||||
this.props.dispatch(signIn())
|
||||
.catch(response => this._parseErrorFromResponse(response));
|
||||
}
|
||||
|
||||
_onRequestGoogleSignIn: () => Object;
|
||||
@@ -132,8 +135,14 @@ class StartLiveStreamDialog
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onRequestGoogleSignIn() {
|
||||
return googleApi.showAccountSelection()
|
||||
.then(() => this._setStateIfMounted({ broadcasts: undefined }))
|
||||
// when there is an error we show the google sign-in button.
|
||||
// once we click it we want to clear the error from the state
|
||||
this.props.dispatch(showAccountSelection())
|
||||
.then(() =>
|
||||
this._setStateIfMounted({
|
||||
broadcasts: undefined,
|
||||
errorType: undefined
|
||||
}))
|
||||
.then(() => this._onGetYouTubeBroadcasts());
|
||||
}
|
||||
|
||||
@@ -151,55 +160,20 @@ class StartLiveStreamDialog
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onYouTubeBroadcastIDSelected(boundStreamID) {
|
||||
return googleApi.requestLiveStreamsForYouTubeBroadcast(boundStreamID)
|
||||
.then(response => {
|
||||
const broadcasts = response.result.items;
|
||||
const streamName = broadcasts
|
||||
&& broadcasts[0]
|
||||
&& broadcasts[0].cdn.ingestionInfo.streamName;
|
||||
const streamKey = streamName || '';
|
||||
|
||||
this.props.dispatch(
|
||||
requestLiveStreamsForYouTubeBroadcast(boundStreamID))
|
||||
.then(({ streamKey, selectedBoundStreamID }) =>
|
||||
this._setStateIfMounted({
|
||||
streamKey,
|
||||
selectedBoundStreamID: boundStreamID
|
||||
});
|
||||
});
|
||||
}
|
||||
selectedBoundStreamID
|
||||
}));
|
||||
|
||||
_parseBroadcasts: (Array<Object>) => Array<Object>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {Array} broadcasts - Broadcast descriptions as obtained from
|
||||
* calling the YouTube API.
|
||||
* @private
|
||||
* @returns {Array} An array of objects describing each unique broadcast.
|
||||
*/
|
||||
_parseBroadcasts(broadcasts) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches in a Google API error response for the error type.
|
||||
* Only show an error if an external request was made with the Google api.
|
||||
* Do not error if the login in canceled.
|
||||
* And searches in a Google API error response for the error type.
|
||||
*
|
||||
* @param {Object} response - The Google API response that may contain an
|
||||
* error.
|
||||
@@ -207,12 +181,19 @@ class StartLiveStreamDialog
|
||||
* @returns {string|null}
|
||||
*/
|
||||
_parseErrorFromResponse(response) {
|
||||
|
||||
if (!response || !response.result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = response.result;
|
||||
const error = result.error;
|
||||
const errors = error && error.errors;
|
||||
const firstError = errors && errors[0];
|
||||
|
||||
return (firstError && firstError.reason) || null;
|
||||
this._setStateIfMounted({
|
||||
errorType: (firstError && firstError.reason) || null
|
||||
});
|
||||
}
|
||||
|
||||
_renderDialogContent: () => React$Component<*>
|
||||
@@ -243,20 +224,22 @@ class StartLiveStreamDialog
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderYouTubePanel() {
|
||||
const { t } = this.props;
|
||||
const {
|
||||
t,
|
||||
_googleProfileEmail
|
||||
} = this.props;
|
||||
const {
|
||||
broadcasts,
|
||||
googleProfileEmail,
|
||||
selectedBoundStreamID
|
||||
} = this.state;
|
||||
|
||||
let googleContent, helpText;
|
||||
|
||||
switch (this.state.googleAPIState) {
|
||||
switch (this.props._googleAPIState) {
|
||||
case GOOGLE_API_STATES.LOADED:
|
||||
googleContent = ( // eslint-disable-line no-extra-parens
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onGetYouTubeBroadcasts }
|
||||
onClick = { this._onGoogleSignIn }
|
||||
text = { t('liveStreaming.signIn') } />
|
||||
);
|
||||
helpText = t('liveStreaming.signInCTA');
|
||||
@@ -279,7 +262,7 @@ class StartLiveStreamDialog
|
||||
helpText = ( // eslint-disable-line no-extra-parens
|
||||
<div>
|
||||
{ `${t('liveStreaming.chooseCTA',
|
||||
{ email: googleProfileEmail })} ` }
|
||||
{ email: _googleProfileEmail })} ` }
|
||||
<a onClick = { this._onRequestGoogleSignIn }>
|
||||
{ t('liveStreaming.changeSignIn') }
|
||||
</a>
|
||||
@@ -288,16 +271,6 @@ class StartLiveStreamDialog
|
||||
|
||||
break;
|
||||
|
||||
case GOOGLE_API_STATES.ERROR:
|
||||
googleContent = ( // eslint-disable-line no-extra-parens
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onRequestGoogleSignIn }
|
||||
text = { t('liveStreaming.signIn') } />
|
||||
);
|
||||
helpText = this._getGoogleErrorMessageToDisplay();
|
||||
|
||||
break;
|
||||
|
||||
case GOOGLE_API_STATES.NEEDS_LOADING:
|
||||
default:
|
||||
googleContent = ( // eslint-disable-line no-extra-parens
|
||||
@@ -309,6 +282,15 @@ class StartLiveStreamDialog
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.state.errorType !== undefined) {
|
||||
googleContent = ( // eslint-disable-line no-extra-parens
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onRequestGoogleSignIn }
|
||||
text = { t('liveStreaming.signIn') } />
|
||||
);
|
||||
helpText = this._getGoogleErrorMessageToDisplay();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'google-panel'>
|
||||
<div className = 'live-stream-cta'>
|
||||
@@ -336,7 +318,7 @@ class StartLiveStreamDialog
|
||||
case 'liveStreamingNotEnabled':
|
||||
text = this.props.t(
|
||||
'liveStreaming.errorLiveStreamNotEnabled',
|
||||
{ email: this.state.googleProfileEmail });
|
||||
{ email: this.props._googleProfileEmail });
|
||||
break;
|
||||
default:
|
||||
text = this.props.t('liveStreaming.errorAPI');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user