Compare commits

...

58 Commits

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

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

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

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

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

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

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

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

The previous code made an incorrect claim about the thread in which the audio
route change notification selector is called: it's called on a secondary thread:
https://developer.apple.com/documentation/avfoundation/avaudiosessionroutechangenotification
2018-07-27 15:39:39 -05:00
Saúl Ibarra Corretgé
243dd16285 android: run all audio and bluetooth operation on a dedicated thread 2018-07-27 15:39:39 -05:00
Saúl Ibarra Corretgé
92001f4d37 android: run WiFi stats operations on a dedicated thread 2018-07-27 15:39:39 -05:00
paweldomas
6a31c59081 ref(media/VideoTrack.native): remove fade animation 2018-07-27 12:08:54 +02:00
paweldomas
11c5b220a1 fix(participants/Avatar.native): disable fade animation
The Image adds a fade effect without asking, so lets explicitly disable
it. More info here:
https://github.com/facebook/react-native/issues/10194
2018-07-27 12:08:54 +02:00
virtuacoplenny
590ad90cd1 ref(video-layout): resize thumbnails first when resizing video area (#3308) 2018-07-26 11:45:04 -07:00
Nik
ca62e902bc Merge pull request #3312 from nikvaessen/master
comment out transcribingEnabled property; in code defaults to false
2018-07-26 19:51:38 +02:00
virtuacoplenny
34d1eb6768 ref(filmstrip): create an empty container for local filmstrip move (#3303)
* ref(filmstrip): create an empty container for local filmstrip move

This might be necessary for tile view. To support making the
local video display at the end of remote videos while in tile
view, but separateed from scrollable remote videos, moving
the local video might be necessary. By creating an empty
container, there is a target for local video to move to.

* squash: rename id
2018-07-26 12:51:15 -05:00
Nik Vaessen
b6b21e5410 comment out transcribingEnabled property; in code defaults to false 2018-07-26 19:10:40 +02:00
Nik
b8daf0a9f9 [WEB] add UI for transcription (#3213)
* [WEB] add UI for transcription

* add analytics event for button, do not use global APP object

* use props instead of state, use local conference to kick participant

* put imports in alphabetical order

* add translation for TranscribingLabel

* fix merge conflict

* add closed caption button

* purge OverFlowMenuItem which starts and stops Transcription

* readd closed caption icon and fix small issues due to purge

* delete unused icon in _font.scss
2018-07-26 09:33:40 -07:00
virtuacoplenny
39f1958300 ref(filmstrip): apply filmstrip class to Conference root (#3294)
* ref(filmstrip): apply filmstrip class to Conference root

Instead of apply the layout class to the body, it can be
applied to Conference. This will allow easier switching
between tile filmstrip and horizontal/vertical filmstrip.

* squash: fix typo filstrip
2018-07-25 13:00:00 -07:00
Leonard Kim
0b1224495b ref(video-quality): update video quality post redux update
Move away from middleware and instead update video quality
when the selected video quality updates in redux. This also
lead to removing of automatically exiting audio only because
with the change it's not so readily possible to tell if the
user switched off audio only by re-selecting the already
preferred video quality. Removing this automagic removed
some additional checking done for mobile.
2018-07-25 12:17:13 -07:00
Leonard Kim
ee7d180cbb feat(video-quality): be able to set an internal max
The internal max will be used for tile view. Whatever the
user has set for preferred video quality, the internal
maximum will be respected. This allows for the case where
the user prefers high definition video, but in tile view
it only makes sense to send low definition; ux wise the
user is allowed to continue messing with the video quality
slider.
2018-07-25 12:17:13 -07:00
Leonard Kim
4d3383c620 ref(video-quality): rename receiveVideoQuality to preferredReceiverVideoQuality
- "preferred" is being appended because in tile view there is a
  concept of what the user prefers to be the maximum video quality
  but there is also a maximum respected internall. For example,
  the user may prefer HD, but in tile view the tiles may be small
  so internall the preferred would be set to LD.
- "receive" is being renamed to "receiver" to be consistent with
  the naming in lib-jitsi-meet.
2018-07-25 12:17:13 -07:00
Pablo Saavedra
fd78203ff8 noticeMessage is not shown (refs #3295)
* Get back the Notice class
* Add Notice component in the Conference web view
* Notice is not exported in index.js. Only used internally by
  Conference.
* noticeMessage value obtained from features/base/config
  * using mapStateToProps
  * value is stored in the internal _message property
* Notice component, orignal in `toolbox` is moved from
  `toolbox/components` to `conference/components`
* Notice component only implemented and renderable in web views
* Dummy `conference/components/Notice.naive.js`

This patch is partially based in the removed logic included
originally in:

    commit 59a74153dc
    (tag: jitsi-meet_1886, tag: jitsi-meet_1885, tag: 1797, tag: 1796)
    Author: Ilya Daynatovich <shupuercha@gmail.com>
    Date:   Mon Mar 20 11:04:54 2017 -0500

      Toolbar notice as React Component

In reply to: Saúl Ibarra Corretgé @saghul> comments

Signed-off-by: Pablo Saavedra <psaavedra@igalia.com>
2018-07-25 14:16:47 -05:00
virtuacoplenny
a36b341865 ref(popover): allow for popover content from the right (#3302)
* ref(popover): allow for popover content from the right

Popovers contents can display to the left of the trigger
and above the trigger. Add the ability to display to the
right of the trigger my adding mouseover padding. This
may be needed for tile view, depending on where the triggers
are located.

* squash: abstract common css proprties into placeholder class
2018-07-25 13:28:36 -05:00
Saúl Ibarra Corretgé
3d6e18394e deps: update url-polyfill dependency
The previous location no longer exists. This is a fork of the original package,
which is actively maintained.

Fixes: #3304
2018-07-25 11:27:54 -05:00
virtuacoplenny
9a6e5c67f5 feat(tile-view): add new toolbar icon (#3292) 2018-07-25 08:22:18 -07:00
virtuacoplenny
50ea847905 Refactor welcome page in prep for branding (#3230)
* fix(welcome-page): css tweaks in prep for branded welcome page

- Watermarks should no longer depend on toolbar size. The left watermark made
  room for the toolbar when the toolbar was on the left side of the screen, but
  the toolbar has been moved to the bottom. The right watermark...well it'll
  clash with the vertical filmstrip but at least the margins will be consistent
  with the left watermark.
- Apply new font-family so fonts are more likely to be consistent across the
  app. Design likes SF UI and keeps requesting it so use it by default.
- Change sizings of welcome page header to be more responsive. This will help
  the header be scrollable when there is no additional content and the header
  overflows.
- Change colors of the welcome page header and remove background image that
  was in the header. Leave in the dom for the background image in case other
  deployments need to continue showing an image.
- Add a period to the title of the welcome page.
- Move watermarks dom location as it is not part of the header; it's part of the
  whole page.

* [squash] Size and font adjustments. Renaming.
2018-07-24 14:26:17 -05:00
virtuacoplenny
b54a9e2bf7 chore(deps): update lib for selecting participants and maxFrameHeight caching (#3291) 2018-07-23 14:20:30 -07:00
virtuacoplenny
918fb1dfc6 ref(utils): use web reportError helper (#3283) 2018-07-21 08:16:32 -07:00
Дамян Минков
9f015df61d Commit from translate.jitsi.org by user damencho.: 446 of 447 strings translated (0 fuzzy). (#3257) 2018-07-20 15:07:29 -05:00
Leonard Kim
2cd1b7f80b fix(presence-label): set position for small video presence label only 2018-07-20 13:27:28 -05:00
virtuacoplenny
afd2aea79c ref(large-video): combine selectParticipant logic from web (#3266)
* ref(large-video): combine selectParticipant logic from web

Currently native/middleware/redux has its own logic for selecting a participant
on the bridge. To have the logic web respect that logic, a few changes are
needed.
- Web no longer has its own call to selectParticipant.
- To keep in line with web logic selectParticipant action should act even when
  there is no track. This makes it so that when a participant does get a track
  that the bridge will send high quality. The bridge can already handle when the
  selected participant does not have a video track.
- The timing of web is such that on joining an existing conference, a
  participant joins and the participant's tracks get updated and then the
  conference is joined. The result is selectParticipant does not get fired
  because it no-ops when there is no conference. To avoid having to make
  uncertain changes (to be lazy), update the selected participant on conference
  join as well.

* squash: update comment, pass message to error handler
2018-07-20 13:19:26 -05:00
virtuacoplenny
c62f761d67 fix(dial-in): allow scroll on dial in info page (#3271)
* fix(dial-in): allow scroll on dial in info page

* squash: some more tweaks for flexible sizing
2018-07-20 10:32:28 -05:00
Boris Grozev
acda279111 npm: Updates lib-jitsi-meet. 2018-07-19 17:10:28 -05:00
Daniel Ornelas
ccf9e2a362 deps: update react-native-webrtc
This version uses a worker queue for all WebRTC operations in iOS.
2018-07-19 18:35:56 +02:00
Saúl Ibarra Corretgé
3154c6f936 [RN] Don't request camera permission on first launch
It will only be requested if a user joins a meeting or flips the switch from
video to audio and back, but never as the first thing when the welcome page is
mounted.
2018-07-19 09:03:22 -05:00
174 changed files with 6018 additions and 3478 deletions

View File

@@ -23,6 +23,7 @@
; seen to cause errors and we have chosen not to fix.
.*/node_modules/@atlaskit/.*/*.js.flow
.*/node_modules/react-native-keep-awake/.*
.*/node_modules/react-native-permissions/.*
.*/node_modules/styled-components/.*
.*/\.git/.*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,8 +7,6 @@ import Recorder from './modules/recorder/Recorder';
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
import { reportError } from './modules/util/helpers';
import * as RemoteControlEvents
from './service/remotecontrol/RemoteControlEvents';
import UIEvents from './service/UI/UIEvents';
@@ -18,7 +16,6 @@ import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import {
createDeviceChangedEvent,
createScreenSharingEvent,
createSelectParticipantFailedEvent,
createStreamSwitchDelayEvent,
createTrackMutedEvent,
sendAnalytics
@@ -38,6 +35,7 @@ import {
conferenceJoined,
conferenceLeft,
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
EMAIL_COMMAND,
lockStateChanged,
@@ -47,6 +45,7 @@ import {
setDesktopSharingEnabled
} from './react/features/base/conference';
import {
getAvailableDevices,
setAudioOutputDeviceId,
updateDeviceList
} from './react/features/base/devices';
@@ -75,6 +74,8 @@ import {
getAvatarURLByParticipantId,
getLocalParticipant,
getParticipantById,
hiddenParticipantJoined,
hiddenParticipantLeft,
localParticipantConnectionStatusChanged,
localParticipantRoleChanged,
MAX_DISPLAY_NAME_LENGTH,
@@ -373,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.
@@ -469,6 +472,7 @@ function _connectionFailedHandler(error) {
JitsiConnectionEvents.CONNECTION_FAILED,
_connectionFailedHandler);
if (room) {
APP.store.dispatch(conferenceWillLeave(room));
room.leave();
}
}
@@ -1657,10 +1661,13 @@ export default {
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
user => APP.UI.onUserFeaturesChanged(user));
room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
const displayName = user.getDisplayName();
if (user.isHidden()) {
APP.store.dispatch(hiddenParticipantJoined(id, displayName));
return;
}
const displayName = user.getDisplayName();
APP.store.dispatch(participantJoined({
botType: user.getBotType(),
@@ -1685,8 +1692,11 @@ export default {
room.on(JitsiConferenceEvents.USER_LEFT, (id, user) => {
if (user.isHidden()) {
APP.store.dispatch(hiddenParticipantLeft(id));
return;
}
APP.store.dispatch(participantLeft(id, room));
logger.log('USER %s LEFT', id, user);
APP.API.notifyUserLeft(id);
@@ -1813,24 +1823,6 @@ export default {
room.sendTextMessage(message);
});
}
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, id => {
APP.API.notifyOnStageParticipantChanged(id);
try {
// do not try to select participant if there is none (we
// are alone in the room), otherwise an error will be
// thrown cause reporting mechanism is not available
// (datachannels currently)
if (room.getParticipants().length === 0) {
return;
}
room.selectParticipant(id);
} catch (e) {
sendAnalytics(createSelectParticipantFailedEvent(e));
reportError(e);
}
});
}
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
@@ -2138,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
@@ -2323,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();
},
/**
@@ -2506,13 +2488,24 @@ export default {
// before all operations are done.
Promise.all([
requestFeedbackPromise,
room.leave().then(disconnect, disconnect)
this.leaveRoomAndDisconnect()
]).then(values => {
APP.API.notifyReadyToClose();
maybeRedirectToWelcomePage(values[0]);
});
},
/**
* Leaves the room and calls JitsiConnection.disconnect.
*
* @returns {Promise}
*/
leaveRoomAndDisconnect() {
APP.store.dispatch(conferenceWillLeave(room));
return room.leave().then(disconnect, disconnect);
},
/**
* Changes the email for the local user
* @param email {string} the new email

View File

@@ -175,6 +175,10 @@ var config = {
// Whether to enable live streaming or not.
// liveStreamingEnabled: false,
// Transcription (in interface_config,
// subtitles and buttons can be configured)
// transcribingEnabled: false,
// Misc
// Default value for the channel "last N" attribute. -1 for unlimited.

View File

@@ -108,14 +108,15 @@ form {
}
.leftwatermark {
left: $defaultToolbarSize;
margin-left: 10px;
left: 32px;
top: 32px;
background-image: url($defaultWatermarkLink);
background-position: center left;
}
.rightwatermark {
right: 15;
right: 32px;
top: 32px;
background-position: center right;
}

View File

@@ -24,6 +24,7 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-arrow_back:before {
content: "\e5c4";
}
@@ -210,3 +211,12 @@
.icon-speaker:before {
content: "\e92d";
}
.icon-tiles-many:before {
content: "\e92e";
}
.icon-tiles-one:before {
content: "\e92f";
}
.icon-closed_caption:before {
content: "\e930";
}

View File

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

View File

@@ -10,14 +10,24 @@
right: 0;
width: 100%;
}
.popover-mousemove-padding-right {
%vertical-popover-padding {
height: 100%;
position: absolute;
right: -20;
top: 0;
width: 40px;
}
.popover-mousemove-padding-left {
@extend %vertical-popover-padding;
left: -20px;
}
.popover-mousemove-padding-right {
@extend %vertical-popover-padding;
right: -20px;
}
/**
* An invisible element is added to the top of the popover to ensure the mouse
* stays over the popover when the popover's height is shrunk, which would then

View File

@@ -146,7 +146,6 @@
background: #B8C7E0;
border-radius: 2px;
color: $newToolbarBackgroundColor;
font-family: -apple-system, BlinkMacSystemFont, $baseFontFamily;
font-size: 11px;
font-weight: bold;
margin-left: 8px;

View File

@@ -3,7 +3,7 @@
/**
* Style variables
*/
$baseFontFamily: 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$baseFontFamily: -apple-system, BlinkMacSystemFont, 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$hangupColor: #bf2117;
$hangupFontSize: 2em;
@@ -144,7 +144,7 @@ $watermarkHeight: 74px;
/**
* Welcome page variables.
*/
$welcomePageDescriptionColor: #fff;
$welcomePageDescriptionColor: #E6EDFA;
$welcomePageFontFamily: inherit;
$welcomePageHeaderBackground: linear-gradient(#165ecc, #44A5FF);
$welcomePageHeaderBackground: #1D69D4;
$welcomePageTitleColor: #fff;

View File

@@ -748,12 +748,10 @@
margin: 0 auto;
overflow: hidden;
pointer-events: none;
position: absolute;
right: 0;
text-align: center;
text-overflow: ellipsis;
top: calc(50% + 30px);
white-space: nowrap;
width: 100%;
z-index: $zindex3;
}

View File

@@ -4,15 +4,19 @@ body.welcome-page {
}
.welcome {
background-color: $welcomePageHeaderBackground;
display: flex;
flex-direction: column;
font-family: $welcomePageFontFamily;
height: 100%;
justify-content: space-between;
min-height: 100vh;
position: relative;
.header {
align-items: center;
background: $welcomePageHeaderBackground;
display: flex;
flex-direction: column;
min-height: fit-content;
overflow: hidden;
position: relative;
text-align: center;
@@ -20,49 +24,42 @@ body.welcome-page {
.header-text {
display: flex;
flex-direction: column;
justify-content: space-around;
margin-top: 120px;
margin-bottom: 20px;
margin-top: $watermarkHeight + 80;
margin-bottom: 36px;
max-width: calc(100% - 40px);
min-height: 286px;
width: 645px;
width: 650px;
z-index: $zindex2;
}
.header-text-title {
color: $welcomePageTitleColor;
font-size: 48px;
letter-spacing: -1px;
line-height: 58px;
margin-bottom: 20px;
font-size: 2.5rem;
font-weight: 500;
letter-spacing: 0;
line-height: 1.18;
margin-bottom: 16px;
}
.header-text-description {
color: $welcomePageDescriptionColor;
font-size: 20px;
line-height: 28px;
opacity: 0.8;
font-size: 1rem;
font-weight: 400;
line-height: 24px;
}
.header-image {
background-image: url(../images/welcome_page/curves.png);
background-size: contain;
height: 209px;
position: absolute;
width: 1070px;
}
#new_enter_room {
#enter_room {
align-items: center;
display: flex;
margin-bottom: 20px;
max-width: calc(100% - 40px);
margin-bottom: 20px;
position: relative;
z-index: 2;
width: 650px;
z-index: $zindex2;
.enter-room-input {
display: inline-block;
margin-right: 15px;
width: 350px;
margin-right: 8px;
width: 100%;
}
}
}
@@ -70,24 +67,10 @@ body.welcome-page {
.welcome-page-button {
font-size: 16px;
}
}
.welcome.with-content {
.header {
min-height: 552px;
}
.header-image {
left: -61px;
top: 401px;
}
}
.welcome.without-content {
.header {
.welcome-watermark {
position: absolute;
width: 100%;
height: 100%;
}
.header-image {
bottom: -20px;
left: 0;
}
}

View File

@@ -48,4 +48,9 @@
object-fit: cover;
overflow: hidden;
}
.presence-label {
position: absolute;
z-index: $zindex3;
}
}

View File

@@ -127,7 +127,7 @@
/**
* Override other styles to support vertical filmstrip mode.
*/
.vertical-filmstrip.filmstrip-only {
.filmstrip-only .vertical-filmstrip {
.filmstrip {
flex-direction: row-reverse;
}

View File

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

View File

@@ -126,11 +126,16 @@
.dial-in-page {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
font-size: 24px;
height: 100%;
justify-content: center;
max-height: 100%;
overflow: auto;
padding: 25px;
position: absolute;
transform: translateY(-50%);
top: 50%;
width: 100%;
.dial-in-numbers-list {
@@ -140,6 +145,7 @@
.dial-in-conference-id {
text-align: center;
min-width: 200px;
width: 30%;
}
}

View File

@@ -155,7 +155,6 @@
.circular-label {
color: white;
font-family: -apple-system, BlinkMacSystemFont, $baseFontFamily;
font-weight: bold;
margin-left: 8px;
opacity: 0.8;

View File

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

Binary file not shown.

View File

@@ -72,4 +72,7 @@
<glyph unicode="&#xe92b;" glyph-name="rec" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM581.333 433.782h-110.595v59.233h104.338v40.332h-104.338v56.87h110.595v43.539h-161.665v-243.512h161.665v43.539zM738.771 384c58.849 0 101.802 36.282 106.029 88.933h-49.717c-4.904-26.832-26.888-44.045-56.143-44.045-38.556 0-62.4 31.895-62.4 83.196s23.844 83.027 62.231 83.027c29.086 0 51.239-18.394 56.143-46.407h49.717c-3.72 52.989-48.026 91.296-105.86 91.296-70.855 0-114.485-48.77-114.485-127.916 0-79.314 43.798-128.084 114.485-128.084zM230.27 478.502h41.769l45.489-88.258h57.834l-51.408 96.19c28.072 11.138 44.306 38.138 44.306 69.189 0 48.432-32.976 78.133-86.582 78.133h-102.478v-243.512h51.070v88.258zM230.27 592.58v-74.927h44.813c25.704 0 40.754 13.838 40.754 37.295 0 23.119-15.896 37.632-41.262 37.632h-44.306z" />
<glyph unicode="&#xe92c;" glyph-name="live" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM298.225 430.025h-112.35v206.5h-52.85v-252.525h165.2v46.025zM399.025 384v252.525h-52.85v-252.525h52.85zM591.525 384l84.175 252.525h-56.875l-56.35-193.025h-3.15l-57.4 193.025h-59.675l85.4-252.525h63.875zM886.050 429.15h-114.45v61.425h107.975v41.825h-107.975v58.975h114.45v45.15h-167.3v-252.525h167.3v45.15z" />
<glyph unicode="&#xe92d;" glyph-name="speaker" d="M0 512c0-282.795 229.205-512 512-512s512 229.205 512 512c0 282.795-229.205 512-512 512s-512-229.205-512-512zM525.005 759.362c-20.475 24.944-16.326 61.342 9.268 81.297s62.94 15.911 83.416-9.033c16.036-19.536 38.593-52.97 60.894-97.797 81.621-164.065 89.461-340.992-26.857-506.352-8.384-11.919-17.386-23.69-27.012-35.307-20.593-24.851-57.959-28.727-83.458-8.657s-29.476 56.487-8.882 81.338c7.686 9.275 14.833 18.621 21.455 28.035 88.66 126.041 82.71 260.306 17.953 390.475-10.599 21.305-21.94 40.51-33.198 57.196-6.515 9.657-11.322 16.057-13.578 18.805zM353.479 647.46c-19.353 24.679-15.129 60.448 9.434 79.893s60.164 15.2 79.517-9.479c9.635-12.287 22.577-32.644 35.209-60.034 50.35-109.176 50.35-231.689-33.639-349.612-18.198-25.551-53.566-31.441-78.997-13.157s-31.294 53.819-13.096 79.37c57.564 80.822 57.564 160.581 22.983 235.565-8.601 18.65-16.892 31.691-21.412 37.455z" />
<glyph unicode="&#xe92e;" glyph-name="tiles-many" d="M113.778 1024h227.556c62.838 0 113.778-50.94 113.778-113.778v-227.556c0-62.838-50.94-113.778-113.778-113.778h-227.556c-62.838 0-113.778 50.94-113.778 113.778v227.556c0 62.838 50.94 113.778 113.778 113.778zM170.667 910.222c-31.419 0-56.889-25.47-56.889-56.889v-113.778c0-31.419 25.47-56.889 56.889-56.889h113.778c31.419 0 56.889 25.47 56.889 56.889v113.778c0 31.419-25.47 56.889-56.889 56.889h-113.778zM113.778 455.111h227.556c62.838 0 113.778-50.94 113.778-113.778v-227.556c0-62.838-50.94-113.778-113.778-113.778h-227.556c-62.838 0-113.778 50.94-113.778 113.778v227.556c0 62.838 50.94 113.778 113.778 113.778zM170.667 341.333c-31.419 0-56.889-25.47-56.889-56.889v-113.778c0-31.419 25.47-56.889 56.889-56.889h113.778c31.419 0 56.889 25.47 56.889 56.889v113.778c0 31.419-25.47 56.889-56.889 56.889h-113.778zM682.667 1024h227.556c62.838 0 113.778-50.94 113.778-113.778v-227.556c0-62.838-50.94-113.778-113.778-113.778h-227.556c-62.838 0-113.778 50.94-113.778 113.778v227.556c0 62.838 50.94 113.778 113.778 113.778zM739.556 910.222c-31.419 0-56.889-25.47-56.889-56.889v-113.778c0-31.419 25.47-56.889 56.889-56.889h113.778c31.419 0 56.889 25.47 56.889 56.889v113.778c0 31.419-25.47 56.889-56.889 56.889h-113.778zM682.667 455.111h227.556c62.838 0 113.778-50.94 113.778-113.778v-227.556c0-62.838-50.94-113.778-113.778-113.778h-227.556c-62.838 0-113.778 50.94-113.778 113.778v227.556c0 62.838 50.94 113.778 113.778 113.778zM739.556 341.333c-31.419 0-56.889-25.47-56.889-56.889v-113.778c0-31.419 25.47-56.889 56.889-56.889h113.778c31.419 0 56.889 25.47 56.889 56.889v113.778c0 31.419-25.47 56.889-56.889 56.889h-113.778z" />
<glyph unicode="&#xe92f;" glyph-name="tiles-one" d="M170.667 810.667h682.667c47.128 0 85.333-38.205 85.333-85.333v-426.667c0-47.128-38.205-85.333-85.333-85.333h-682.667c-47.128 0-85.333 38.205-85.333 85.333v426.667c0 47.128 38.205 85.333 85.333 85.333zM213.333 725.333c-23.564 0-42.667-19.103-42.667-42.667v-341.333c0-23.564 19.103-42.667 42.667-42.667h597.333c23.564 0 42.667 19.103 42.667 42.667v341.333c0 23.564-19.103 42.667-42.667 42.667h-597.333z" />
<glyph unicode="&#xe930;" glyph-name="closed_caption" d="M768 554v44c0 24-18 42-42 42h-128c-24 0-44-18-44-42v-172c0-24 20-42 44-42h128c24 0 42 18 42 42v44h-64v-22h-86v128h86v-22h64zM470 554v44c0 24-20 42-44 42h-128c-24 0-42-18-42-42v-172c0-24 18-42 42-42h128c24 0 44 18 44 42v44h-64v-22h-86v128h86v-22h64zM810 854c46 0 86-40 86-86v-512c0-46-40-86-86-86h-596c-48 0-86 40-86 86v512c0 46 38 86 86 86h596z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

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

View File

@@ -45,10 +45,10 @@ var interfaceConfig = {
* jwt.
*/
TOOLBAR_BUTTONS: [
'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
'profile', 'info', 'chat', 'recording', 'livestreaming', 'etherpad',
'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip',
'invite', 'feedback', 'stats', 'shortcuts'
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
@@ -163,7 +163,14 @@ var interfaceConfig = {
*
* @type {boolean}
*/
VIDEO_QUALITY_LABEL_DISABLED: false
VIDEO_QUALITY_LABEL_DISABLED: false,
/**
* If true, will display recent list
*
* @type {boolean}
*/
RECENT_LIST_ENABLED: true
/**
* Specify custom URL for downloading android mobile app.

View File

@@ -28,13 +28,15 @@ 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',
:path => '../node_modules/react-native-locale-detector'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'ReactNativePermissions',
:path => '../node_modules/react-native-permissions'
pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'react-native-calendar-events',

View File

@@ -1,6 +1,7 @@
PODS:
- boost-for-react-native (1.63.0)
- DoubleConversion (1.1.5)
- FLAnimatedImage (1.0.12)
- Folly (2016.09.26.00):
- boost-for-react-native
- DoubleConversion
@@ -12,8 +13,11 @@ PODS:
- React
- react-native-calendar-events (1.6.0):
- React
- react-native-fetch-blob (0.10.6):
- React/Core
- react-native-fast-image (4.0.14):
- FLAnimatedImage
- React
- SDWebImage/Core
- SDWebImage/GIF
- react-native-keep-awake (2.0.6):
- React
- react-native-locale-detector (1.0.0):
@@ -59,6 +63,8 @@ PODS:
- React/Core
- React/fishhook
- React/RCTBlob
- ReactNativePermissions (1.1.1):
- React
- RNSound (0.10.9):
- React/Core
- RNSound/Core (= 0.10.9)
@@ -66,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:
@@ -74,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`)
@@ -88,6 +98,7 @@ DEPENDENCIES:
- React/RCTNetwork (from `../node_modules/react-native`)
- React/RCTText (from `../node_modules/react-native`)
- React/RCTWebSocket (from `../node_modules/react-native`)
- ReactNativePermissions (from `../node_modules/react-native-permissions`)
- RNSound (from `../node_modules/react-native-sound`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -95,6 +106,8 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/cocoapods/specs.git:
- boost-for-react-native
- FLAnimatedImage
- SDWebImage
EXTERNAL SOURCES:
DoubleConversion:
@@ -109,14 +122,16 @@ 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:
:path: "../node_modules/react-native-locale-detector"
react-native-webrtc:
:path: "../node_modules/react-native-webrtc"
ReactNativePermissions:
:path: "../node_modules/react-native-permissions"
RNSound:
:path: "../node_modules/react-native-sound"
RNVectorIcons:
@@ -127,19 +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: fb12a5ae406b901e95aeb1ab5ebbb02773c46ede
PODFILE CHECKSUM: 1d5c8382f73d9540fac68d93b32e1d3b58d069ee
COCOAPODS: 1.5.3

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
{
"contactlist": "__count__ Teilnehmer",
"contactlist_plural": "",
"contactlist_plural": "__count__ Teilnehmer",
"passwordSetRemotely": "von einem anderen Teilnehmer gesetzt",
"poweredby": "Betrieben von",
"inviteUrlDefaultMsg": "Die Konferenz wird erstellt...",
@@ -23,6 +22,7 @@
"react-nativeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b> wenn der Browser um Berechtigungen bittet.",
"chromeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b> wenn der Browser um Berechtigungen bittet.",
"androidGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b> wenn der Browser um Berechtigungen bittet.",
"electronGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons erteilen",
"firefoxGrantPermissions": "Wählen Sie <b><i>Markiertes Gerät teilen</i></b> wenn der Browser um Berechtigungen bittet.",
"operaGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b> wenn der Browser um Berechtigungen bittet.",
"iexplorerGrantPermissions": "Wählen Sie <b><i>OK</i></b> wenn der Browser um Berechtigungen bittet.",
@@ -46,46 +46,20 @@
"showSpeakerStats": "Statistiken für Sprecher anzeigen"
},
"welcomepage": {
"disable": "Diesen Hinweis nicht mehr anzeigen",
"feature1": {
"content": "Kein Download nötig. __app__ läuft direkt im Browser. Einfach die Konferenzadresse teilen und los geht's.",
"title": "Einfach zu benutzen"
},
"feature2": {
"content": "Videokonferenzen mit mehreren Teilnehmen mit weniger als 128Kpbs. Bildschirmfreigaben und Telefonkonferenzen kommen sogar mit noch weniger Bandbreite aus.",
"title": "Niedrige Bandbreite"
},
"feature3": {
"content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäss dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten.",
"title": "Open Source"
},
"feature4": {
"content": "Es gibt keine künstlichen Begrenzungen der Anzahl der Konferenz-Teilnehmer. Die Bandbreite und Rechenleistung des Server sind die einzigen Limitierungen.",
"title": "Unbegrenzte Anzahl Benutzer"
},
"feature5": {
"content": "Es ist ganz einfach den Bildschirm zu teilen. __app__ ist ideal für Online-Präsentationen, Vorlesungen und Fernwartungsanfragen.",
"title": "Bildschirmfreigabe"
},
"feature6": {
"content": "Privatsphäre gewünscht? __app__ Konferenzen können mit einem Passwort geschützt werden um ungebetene Gäste fernzuhalten und Unterbrechungen zu vermeiden.",
"title": "Sichere Konferenzen"
},
"feature7": {
"content": "__app__ verwendet Etherpad, ein Editor zur kollaborativen Bearbeitung von Texten.",
"title": "Freigegebene Notizen"
},
"feature8": {
"content": "Die Verwendung kann durch die Integration mit Piwik, Google Analytics und anderen Überwachungs- und Statistikprogrammen protokolliert werden.",
"title": "Benutzungsstatistiken"
"appDescription": "Auf geht's! Beginne eine Videokonferenz mit dem ganzen Team. Oder eigentlich, lade alle ein die du kennst. __app__ ist eine vollständig verschlüsselte, aus 100% Open-Source-Software bestehende Videokonferenzlösung die du den ganzen Tag kostenlos verwenden kannst — ohne Registrierung.",
"audioVideoSwitch": {
"audio": "Sprache",
"video": "Video"
},
"calendar": "Kalender",
"go": "Los",
"join": "Beitreten",
"privacy": "Privatsphäre",
"roomname": "Konferenzname eingeben",
"roomnamePlaceHolder": "Konferenzname",
"roomnameHint": "Name oder URL der Konferenz der Sie beitreten möchten. Sie können einen Namen erfinden, er muss nur den anderen Teilnehmern übermittelt werden damit sie der gleichen Konferenz beitreten.",
"sendFeedback": "Senden Sie uns Ihr Feedback",
"terms": "Bedingungen"
"terms": "Bedingungen",
"title": "Sichere, flexible und vollständig freie Videokonferenzen"
},
"startupoverlay": {
"policyText": "",
@@ -99,22 +73,28 @@
"toolbar": {
"addPeople": "Teilnehmer zur Konferenz hinzufügen",
"audioonly": "Nur-Audio-Modus aktivieren/deaktivieren (spart Bandbreite)",
"callQuality": "Qualitätseinstellungen",
"enterFullScreen": "Vollbildmodus",
"exitFullScreen": "Vollbildmodus verlassen",
"feedback": "Feedback hinterlasen",
"moreActions": "Weitere Einstellungen",
"mute": "Stummschaltung aktivieren / deaktivieren",
"videomute": "Kamera starten / stoppen",
"authenticate": "Anmelden",
"lock": "Konferenz schützen / Schutz aufheben",
"invite": "Link teilen",
"chat": "Chat öffnen / schliessen",
"etherpad": "Geteiltes Dokument öffnen / schliessen",
"documentOpen": "Geteiltes Dokument öffnen",
"documentClose": "Geteiltes Dokument schliessen",
"sharedvideo": "YouTube-Video teilen",
"sharescreen": "Bildschirmfreigabe starten / stoppen",
"sharescreen": "Bildschirmfreigabe",
"stopSharedVideo": "YouTube Video stoppen",
"fullscreen": "Vollbildmodus aktivieren / deaktivieren",
"sip": "SIP Nummer anrufen",
"Settings": "Einstellungen",
"hangup": "Verlassen",
"login": "Anmelden",
"logout": "Abmelden",
"dialpad": "Wähltastatur öffnen / schliessen",
"sharedVideoMutedPopup": "Das freigegebene Video wurde stumm geschaltet um mit den anderen Teilnehmern zu sprechen.",
"micMutedPopup": "Das Mikrofon wurde stumm geschaltet um das freigegebene Video geniessen zu können.",
"talkWhileMutedPopup": "Versuchen sie zu sprechen? Ihr Mikrofon ist stummgeschaltet.",
@@ -123,17 +103,9 @@
"micDisabled": "Kein Mikrofon verfügbar",
"filmstrip": "Videos anzeigen / verbergen",
"profile": "Profil bearbeiten",
"raiseHand": "Hand erheben"
},
"unsupportedBrowser": {
"appNotInstalled": "Mit __app__ auf dem Mobiltelefon teilnehmen.",
"downloadApp": "App herunterladen",
"openApp": "In __app__ fortfahren"
},
"bottomtoolbar": {
"chat": "Chat öffnen / schliessen",
"filmstrip": "Videos anzeigen / verbergen",
"contactlist": "Teilnehmerliste und neue Teilnehmer einladen"
"raiseHand": "Hand erheben",
"shortcuts": "Tastenkürzel anzeigen",
"speakerStats": "Sprecher-Statistiken"
},
"chat": {
"nickname": {
@@ -218,10 +190,11 @@
"grantedToUnknown": "Moderatorenrechte an $t(notify.somebody) vergeben.",
"muted": "Der Konferenz wurde stumm beigetreten.",
"mutedTitle": "Stummschaltung aktiv!",
"raisedHand": "Möchte sprechen."
"raisedHand": "Möchte sprechen.",
"suboptimalExperienceTitle": "Browserwarnung",
"suboptimalExperienceDescription": "Tut uns leid, aber die Konferenz wird mit __appName__ kein grossartiges Erlebnis. Wir versuchen immer die Situation zu verbessern, bis dahin empfehlen wir aber die Verwendung einer der <a href=\"static/recommendedBrowsers.html\" target=\"_blank\">vollständig unterstützen Browser</a>."
},
"dialog": {
"add": "Hinzufügen",
"allow": "Erlauben",
"kickMessage": "Oh! Sie wurden aus der Konferenz ausgeschlossen.",
"popupErrorTitle": "Popup blockiert",
@@ -236,7 +209,6 @@
"copy": "Kopieren",
"contactSupport": "Support kontaktieren",
"error": "Fehler",
"createPassword": "Passwort erstellen",
"detectext": "Fehler bei der Erkennung der Bildschirmfreigabeerweiterung.",
"failedpermissions": "Die Zugriffsberechtigungen auf das Mikrofon und/oder die Kamera konnten nicht eingeholt werden.",
"conferenceReloadTitle": "Leider ist etwas schiefgegangen.",
@@ -287,10 +259,6 @@
"Save": "Speichern",
"recording": "Aufnahme",
"recordingToken": "Aufnahme-Token eingeben",
"passwordCheck": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?",
"passwordMsg": "Passwort setzen um die Konferenz zu schützen",
"shareLink": "Link zu dieser Konferenz teilen",
"yourPassword": "Neues Passwort eingeben",
"Back": "Zurück",
"serviceUnavailable": "Dienst nicht verfügbar",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
@@ -304,15 +272,14 @@
"tokenAuthFailed": "Sie sind nicht berechtigt dieser Konferenz beizutreten.",
"displayNameRequired": "Anzeigename ist erforderlich",
"enterDisplayName": "Geben Sie Ihren Anzeigenamen ein",
"extensionRequired": "Erweiterung erforderlich:",
"firefoxExtensionPrompt": "Um die Bildschirmfreigabe nutzen zu können, muss eine Firefox-Erweiterung installiert werden. Bitte versuchen Sie es erneut nachdem die <a href='__url__'>Erweiterung installiert</a> wurde.",
"feedbackHelp": "Ihr Feedback hilft uns die Qualität der Konferenzen zu verbessern.",
"feedbackQuestion": "Anmerkungen zur Konferenz.",
"thankYou": "Danke für die Verwendung von __appName__!",
"sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?",
"liveStreaming": "Live-Streaming",
"streamKey": "Streamname/-schlüssel",
"streamKey": "Name/Schlüssel für den Stream",
"startLiveStreaming": "Live-Streaming starten",
"startRecording": "Aufnahme starten",
"stopStreamingWarning": "Sind Sie sicher dass Sie das Live-Streaming stoppen möchten?",
"stopRecordingWarning": "Sind Sie sicher dass Sie die Aufnahme stoppen möchten?",
"stopLiveStreaming": "Live-Streaming stoppen",
@@ -321,6 +288,8 @@
"permissionDenied": "Zugriff verweigert",
"screenSharingFailedToInstall": "Oh! Die Erweiterung für die Bildschirmfreigabe konnte nicht installiert werden.",
"screenSharingFailedToInstallTitle": "Bildschirmfreigabe-Erweiterung konnte nicht installiert werden",
"screenSharingFirefoxPermissionDeniedError": "Die Bildschirmfreigabe ist leider fehlgeschlagen. Bitte stellen Sie sicher, dass die Berechtigung für die Bildschirmfreigabe im Browser erteilt wurde.",
"screenSharingFirefoxPermissionDeniedTitle": "Die Bildschirmfreigabe konnte nicht gestartet werden.",
"screenSharingPermissionDeniedError": "Oh! Beim Anfordern der Bildschirmfreigabe-Berechtigungen hat etwas nicht funktioniert. Bitte aktualisieren und erneut versuchen.",
"cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
"cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.",
@@ -417,14 +386,21 @@
"busy": "Es werden Resourcen zum Streamen bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
"busyTitle": "Alle Streaming-Instanzen sind in Gebrauch",
"buttonTooltip": "Live-Stream starten / stoppen",
"changeSignIn": "Konten wechseln.",
"choose": "Live stream auswählen",
"chooseCTA": "Streaming-Option auswählen. Sie sind aktuell als __email__ angemeldet.",
"enterStreamKey": "Name/Schlüssel für den YouTube Livestream hier eingeben.",
"error": "Das Live-Streaming ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
"errorAPI": "Beim abrufen der YouTube Livestreams ist ein Fehler aufgetreten. Bitte versuchen Sie sich erneut anzumelden.",
"failedToStart": "Live-Streaming konnte nicht gestartet werden",
"off": "Live-Streaming gestoppt",
"on": "Live-Streaming",
"pending": "Live-Stream wird gestartet...",
"serviceName": "Live Streaming-Dienst",
"streamIdRequired": "Bitte Stream-ID eingeben um das Live-Streaming zu starten.",
"streamIdHelp": "Wo ist die Stream-ID zu finden?",
"signIn": "Mit Google anmelden",
"signInCTA": "Anmelden oder den Name/Schlüssel des YouTube Livestreams eingeben.",
"start": "Einen Livestream starten",
"streamIdHelp": "Was ist das?",
"unavailableTitle": "Live-Streaming nicht verfügbar"
},
"videoSIPGW": {
@@ -454,26 +430,16 @@
"selectADevice": "Ein Gerät wählen",
"testAudio": "Audio testen"
},
"invite": {
"addPassword": "Passwort hinzufügen",
"callNumber": "__number__ anrufen",
"enterID": "Um mit einem Telefon teilzunehmen, geben Sie die Konferenz ID (__conferenceID__) gefolgt von # ein",
"howToDialIn": "Wählen Sie eine der folgenden Nummern um via Telefon teilzunehmen und die Konferenz ID",
"hidePassword": "Passwort verstecken",
"inviteTo": "Teilnehmer zu __conferenceName__ einladen",
"invitedYouTo": "Sie wurden von __userName__ zur Konferenz __inviteURL__ eingeladen",
"invitePeople": "Einladen",
"locked": "Diese Konferenz ist gesperrt. Neue Teilnehmer müssen das Passwort eingeben um beizutreten.",
"showPassword": "Passwort anzeigen",
"unlocked": "Die Konferenz ist nicht geschützt. Jeder mit dem Link kann der Konferenz beitreten."
},
"videoStatus": {
"callQuality": "Konferenzqualität",
"hd": "HD",
"hdTooltip": "Video wird in HD angezeigt",
"highDefinition": "Hohe Auflösung",
"labelTooltipVideo": "Aktuelle Videoqualität",
"labelTooltipAudioOnly": "Nur-Audio Modus aktiv",
"labelTooiltipNoVideo": "Kein Video",
"labelTooltipVideo": "Aktuelle Videoqualität",
"ld": "LD",
"ldTooltip": "Video wird in niedriger Auflösung angezeigt",
"lowDefinition": "Niedrige Auflösung",
"onlyAudioAvailable": "Nur Ton",
"onlyAudioSupported": "In diesem Browser wird nur Audio unterstützt.",
@@ -481,21 +447,30 @@
"p2pVideoQualityDescription": "Im Ende-zu-Ende Modus kann die Konferenzqualität nur zwischen hoch und nur-Audio gewählt werden. Andere Einstellungen werden nicht beachtet.",
"recHighDefinitionOnly": "Hohe Qualität wird bevorzugt.",
"sd": "SD",
"sdTooltip": "Video wird in Standardauflösung angezeigt",
"standardDefinition": "Standardauflösung",
"qualityButtonTip": "Empfangene Videoqualität ändern"
},
"dialOut": {
"dial": "Wählen",
"dialOut": "Nummer anrufen",
"statusMessage": "ist jetzt __status__",
"enterPhone": "Telefonnummer eingeben",
"phoneNotAllowed": "Diese Telefonnummer wird leider noch nicht unterstützt!"
"statusMessage": "ist jetzt __status__"
},
"addPeople": {
"add": "Hinzufügen",
"add": "Einladen",
"countryNotSupported": "Wir unterstützen dieses Land noch nicht.",
"countryReminder": "Telefonnummer nicht in den USA? Bitte sicherstellen, dass die Telefonnummer mit dem Ländercode beginnt.",
"disabled": "",
"invite": "Einladen",
"loading": "Suche nach Teilnehmern und Telefonnummern",
"loadingNumber": "Telefonnummer wird überprüft",
"loadingPeople": "Suche nach einzuladenden Teilnehmern",
"noResults": "Keine passenden Ergebnisse",
"searchPlaceholder": "Nach Teilnehmern und Konferenzen suchen",
"title": "Teilnehmer zur Konferenz hinzufügen",
"noValidNumbers": "Telefonnummer eingeben",
"notAvailable": "Sie können keine Teilnehmer einladen.",
"searchNumbers": "Telefonnummern hinzufügen",
"searchPeople": "Nach Teilnehmern suchen",
"searchPeopleAndNumbers": "Nach Teilnehmen suchen oder deren Telefonnummern hinzufügen",
"telephone": "Telefon: __number__",
"title": "Teilnehmer zu dieser Konferenz einladen",
"failedToAdd": "Teilnehmer konnte nicht hinzugefügt werden"
},
"inlineDialogFailure": {
@@ -514,14 +489,71 @@
"average": "Durschnittlich",
"bad": "Schlecht",
"good": "Gut",
"rateExperience": "Bitte bewerten Sie diese Konferenz.",
"detailsLabel": "Sagen Sie uns mehr dazu.",
"rateExperience": "Bitte bewerten Sie diese Konferenz",
"veryBad": "Sehr schlecht",
"veryGood": "Sehr gut"
},
"info": {
"copy": "Link kopieren",
"invite": "In __app__ einladen",
"title": "Konferenz-Zugriffsinformationen",
"addPassword": "Passwort hinzufügen",
"cancelPassword": "Password abbrechen",
"conferenceURL": "Link:",
"country": "Land",
"dialANumber": "Um dieser Konferenz beizutreten, rufen Sie eine dieser Telefonnummern an und geben die PIN __conferenceID__# ein.",
"dialInNumber": "Einwählen:",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "Tut uns leid, einwählen ist momentan nicht unterstützt.",
"genericError": "Es ist leider etwas schiefgegangen.",
"inviteLiveStream": "Klicken Sie auf __url__ um den Livestream dieser Konferenz zu öffnen",
"invitePhone": "Um der Konferenz telefonisch beizutreten, rufen Sie __number__ and und geben die PIN __conferenceID__# ein",
"invitePhoneAlternatives": "Klicken sie auf __url__ um mehr Telefonnummern anzuzeigen",
"inviteURL": "Klicken Sie auf __url__ um der Konferenz beizutreten",
"liveStreamURL": "Livestream:",
"moreNumbers": "Weitere Telefonnummern",
"noNumbers": "Keine Telefonnummern verfügbar.",
"noPassword": "Kein",
"noRoom": "Keine Konferenz für die Einwähl-Informationen angegeben.",
"numbers": "Einwählnummern",
"password": "Passwort:",
"title": "Teilen",
"tooltip": "Zugriffsinformationen über die Konferenz abrufen"
},
"settingsView": {
"alertOk": "OK",
"alertTitle": "Warnung",
"alertURLText": "Die angegebene Server URL ist ungültig",
"conferenceSection": "Konferenz",
"displayName": "Anzeigename",
"email": "E-Mail",
"header": "Einstellungen",
"profileSection": "Profil",
"serverURL": "Server URL",
"startWithAudioMuted": "Stumm beitreten",
"startWithVideoMuted": "Ohne Video beitreten"
},
"calendarSync": {
"later": "Später",
"next": "Folgend",
"nextMeeting": "Nächste Konferenz",
"now": "Jetzt",
"permissionButton": "Einstellungen öffnen",
"permissionMessage": "Die App benötigt Zugriff auf den Kalender um die Termine und Konferenzen anzuzeigen."
},
"recentList": {
"today": "Heute",
"yesterday": "Gestern",
"earlier": "Früher"
},
"sectionList": {
"pullToRefresh": "Ziehen um zu aktualisieren"
},
"deepLinking": {
"title": "Die Konferenz wird in __app__ geöffnet...",
"description": "Nichts passiert? Wir haben versucht die Konferenz in __app__ zu öffnen. Versuchen Sie es erneut oder treten Sie der Konferenz in __app__ im Web bei.",
"tryAgainButton": "Erneut mit der nativen Applikation versuchen",
"launchWebButton": "Im Web öffnen",
"appNotInstalled": "Sie benötigen die __app__ App um der Konferenz auf dem Smartphone beizutreten.",
"downloadApp": "App herunterladen",
"openApp": "In der App fortfahren"
}
}

View File

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

View File

@@ -64,7 +64,7 @@
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",
"terms": "Terms",
"title": "More secure, more flexible, and completely free video conferencing"
"title": "More secure, more flexible, and completely free video conferencing."
},
"startupoverlay": {
"policyText": " ",
@@ -81,6 +81,7 @@
"audioRoute": "Select the sound device",
"callQuality": "Manage call quality",
"chat": "Toggle chat window",
"cc": "Toggle subtitles",
"document": "Toggle shared document",
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
@@ -381,7 +382,8 @@
"shareYourScreenDisabled": "Screen sharing disabled.",
"shareYourScreenDisabledForGuest": "Guests can't screen share.",
"yourEntireScreen": "Your entire screen",
"applicationWindow": "Application window"
"applicationWindow": "Application window",
"transcribing": "Transcribing"
},
"email":
{
@@ -448,6 +450,16 @@
"unavailable": "Oops! The __serviceName__ is currently unavailable. We're working on resolving the issue. Please try again later.",
"unavailableTitle": "Recording unavailable"
},
"transcribing":
{
"pending" : "Preparing to transcribe the meeting...",
"off" : "Transcribing stopped",
"error": "Transcribing failed. Please try again.",
"failedToStart": "Transcribing failed to start",
"tr": "TR",
"labelToolTip": "The meeting is being transcribed",
"ccButtonTooltip": "Start / Stop showing subtitles"
},
"liveStreaming":
{
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
@@ -614,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"
@@ -644,6 +654,11 @@
"ignored": "Ignored",
"expired": "Expired"
},
"dateUtils": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
},
"incomingCall": {
"answer": "Answer",
"audioCallTitle": "Incoming call",

View File

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

View File

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

View File

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

View File

@@ -320,11 +320,6 @@ UI.start = function() {
SidePanels.init(eventEmitter);
}
const filmstripTypeClassname = interfaceConfig.VERTICAL_FILMSTRIP
? 'vertical-filmstrip' : 'horizontal-filmstrip';
$('body').addClass(filmstripTypeClassname);
document.title = interfaceConfig.APP_NAME;
};

View File

@@ -50,10 +50,14 @@ SharedVideoThumb.prototype.createContainer = function(spanId) {
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
const remotes = document.getElementById('filmstripRemoteVideosContainer');
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return remotes.appendChild(container);
return container;
};
/**

View File

@@ -163,6 +163,8 @@ RemoteVideo.prototype._generatePopupContent = function() {
const onVolumeChange = this._setAudioVolume;
const { isModerator } = APP.conference;
const participantID = this.id;
const menuPosition = interfaceConfig.VERTICAL_FILMSTRIP
? 'left bottom' : 'top center';
ReactDOM.render(
<Provider store = { APP.store }>
@@ -172,6 +174,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
initialVolumeValue = { initialVolumeValue }
isAudioMuted = { this.isAudioMuted }
isModerator = { isModerator }
menuPosition = { menuPosition }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }
@@ -642,10 +645,14 @@ RemoteVideo.createContainer = function(spanId) {
<div class ='presence-label-container'></div>
<span class = 'remotevideomenu'></span>`;
const remotes = document.getElementById('filmstripRemoteVideosContainer');
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return remotes.appendChild(container);
return container;
};
export default RemoteVideo;

View File

@@ -854,6 +854,9 @@ const VideoLayout = {
resizeVideoArea(
forceUpdate = false,
animate = false) {
// Resize the thumbnails first.
this.resizeThumbnails(forceUpdate);
if (largeVideo) {
largeVideo.updateContainerSize();
largeVideo.resize(animate);
@@ -866,9 +869,6 @@ const VideoLayout = {
if (availableWidth < 0 || availableHeight < 0) {
return;
}
// Resize the thumbnails first.
this.resizeThumbnails(forceUpdate);
},
getSmallVideo(id) {
@@ -960,7 +960,7 @@ const VideoLayout = {
// FIXME video type is not the same thing as container type
if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
APP.API.notifyOnStageParticipantChanged(id);
}
let oldSmallVideo;

View File

@@ -1,5 +1,3 @@
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Create deferred object.
*
@@ -15,14 +13,3 @@ export function createDeferred() {
return deferred;
}
/**
* Prints the error and reports it to the global error handler.
*
* @param e {Error} the error
* @param msg {string} [optional] the message printed in addition to the error
*/
export function reportError(e, msg = '') {
logger.error(msg, e);
window.onerror && window.onerror(msg, null, null, null, e);
}

86
package-lock.json generated
View File

@@ -3003,6 +3003,11 @@
"sdp-transform": "2.3.0"
}
},
"@webcomponents/url": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@webcomponents/url/-/url-0.7.1.tgz",
"integrity": "sha512-9oFDpuZ+tAogjPYQPhNEX86Npzb73A4kv9DOPsSO9aWoWgoevoP6eKx+TKMwO8BJxtTpSM9nKenHQTJ56SP2Cw=="
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -5568,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",
@@ -5727,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"
}
@@ -6231,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"
@@ -6240,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",
@@ -6277,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"
@@ -9455,7 +9459,7 @@
},
"jitsi-meet-logger": {
"version": "github:jitsi/jitsi-meet-logger#6fff754a77a56ab52499f3559105a15886942a1e",
"from": "jitsi-meet-logger@github:jitsi/jitsi-meet-logger#6fff754a77a56ab52499f3559105a15886942a1e"
"from": "github:jitsi/jitsi-meet-logger#6fff754a77a56ab52499f3559105a15886942a1e"
},
"jquery": {
"version": "3.3.1",
@@ -9519,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",
@@ -9710,8 +9719,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#05b7ae60dc669a69b293fb0e93688fc0a78adb80",
"from": "github:jitsi/lib-jitsi-meet#05b7ae60dc669a69b293fb0e93688fc0a78adb80",
"version": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
"from": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
"requires": {
"@jitsi/sdp-interop": "0.1.13",
"@jitsi/sdp-simulcast": "0.2.1",
@@ -10502,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",
@@ -12699,7 +12713,7 @@
},
"react-native-calendar-events": {
"version": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9",
"from": "react-native-calendar-events@github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9"
"from": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9"
},
"react-native-callstats": {
"version": "3.52.0",
@@ -12710,35 +12724,12 @@
"jssha": "^2.2.0"
}
},
"react-native-fetch-blob": {
"version": "github:joltup/react-native-fetch-blob#1f9a1761aea4e37bd672bd0d233f3adf0e113a11",
"from": "react-native-fetch-blob@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": {
@@ -12761,7 +12752,12 @@
},
"react-native-locale-detector": {
"version": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
"from": "react-native-locale-detector@github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9"
"from": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9"
},
"react-native-permissions": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-1.1.1.tgz",
"integrity": "sha512-t0Ujm177bagjUOSzhpmkSz+LqFW04HnY9TeZFavDCmV521fQvFz82aD+POXqWsAdsJVOK3umJYBNNqCjC3g0hQ=="
},
"react-native-prompt": {
"version": "1.0.0",
@@ -12806,8 +12802,8 @@
}
},
"react-native-webrtc": {
"version": "github:jitsi/react-native-webrtc#b43081ec8357335ab82a2e236c0f6a6ed0b17f9f",
"from": "github:jitsi/react-native-webrtc#b43081ec8357335ab82a2e236c0f6a6ed0b17f9f",
"version": "github:jitsi/react-native-webrtc#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
"from": "github:jitsi/react-native-webrtc#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
"requires": {
"base64-js": "^1.1.2",
"event-target-shim": "^1.0.5",
@@ -13307,9 +13303,9 @@
"integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw=="
},
"rtcpeerconnection-shim": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.12.tgz",
"integrity": "sha512-7v/mpRyCRjVrTr3pqI9ouXzKGtbSg+iJ54fERZCGYc6AwvkTe6WWeKWG4OmG0H3dWkr1NNASyGieE0Q0hMJviQ==",
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.13.tgz",
"integrity": "sha512-Xz4zQLZNs9lFBvqbaHGIjLWtyZ1V82ec5r+WNEo7NlIx3zF5M3ytn9mkkfYeZmpE032cNg3Vvf0rP8kNXUNd9w==",
"requires": {
"sdp": "^2.6.0"
}
@@ -15833,10 +15829,6 @@
}
}
},
"url-polyfill": {
"version": "github:github/url-polyfill#39734186de44612bc5a16eb25f5407adcc5b2e7c",
"from": "url-polyfill@github:github/url-polyfill#39734186de44612bc5a16eb25f5407adcc5b2e7c"
},
"use": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz",

View File

@@ -34,9 +34,8 @@
"@atlaskit/tabs": "4.0.1",
"@atlaskit/theme": "2.4.0",
"@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",
@@ -46,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#05b7ae60dc669a69b293fb0e93688fc0a78adb80",
"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",
@@ -59,21 +60,20 @@
"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",
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
"react-native-permissions": "1.1.1",
"react-native-prompt": "1.0.0",
"react-native-sound": "0.10.9",
"react-native-vector-icons": "4.4.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#b43081ec8357335ab82a2e236c0f6a6ed0b17f9f",
"react-native-webrtc": "github:jitsi/react-native-webrtc#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
"react-redux": "5.0.7",
"redux": "4.0.0",
"redux-thunk": "2.2.0",
"styled-components": "1.4.6",
"url-polyfill": "github:github/url-polyfill#39734186de44612bc5a16eb25f5407adcc5b2e7c",
"uuid": "3.1.0",
"xmldom": "0.1.27"
},

View File

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

View File

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

View File

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

View File

@@ -152,6 +152,19 @@ export const SET_FOLLOW_ME = Symbol('SET_FOLLOW_ME');
*/
export const SET_LASTN = Symbol('SET_LASTN');
/**
* The type of (redux) action which sets the maximum video height that should be
* received from remote participants, even if the user prefers a larger video
* height.
*
* {
* type: SET_MAX_RECEIVER_VIDEO_QUALITY,
* maxReceiverVideoQuality: number
* }
*/
export const SET_MAX_RECEIVER_VIDEO_QUALITY
= Symbol('SET_MAX_RECEIVER_VIDEO_QUALITY');
/**
* The type of (redux) action which sets the password to join or lock a specific
* {@code JitsiConference}.
@@ -177,15 +190,16 @@ export const SET_PASSWORD = Symbol('SET_PASSWORD');
export const SET_PASSWORD_FAILED = Symbol('SET_PASSWORD_FAILED');
/**
* The type of (redux) action which sets the maximum video size should be
* received from remote participants.
* The type of (redux) action which sets the preferred maximum video height that
* should be received from remote participants.
*
* {
* type: SET_RECEIVE_VIDEO_QUALITY,
* receiveVideoQuality: number
* type: SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
* preferredReceiverVideoQuality: number
* }
*/
export const SET_RECEIVE_VIDEO_QUALITY = Symbol('SET_RECEIVE_VIDEO_QUALITY');
export const SET_PREFERRED_RECEIVER_VIDEO_QUALITY
= Symbol('SET_PREFERRED_RECEIVER_VIDEO_QUALITY');
/**
* The type of (redux) action which sets the name of the room of the

View File

@@ -37,9 +37,10 @@ import {
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_LASTN,
SET_MAX_RECEIVER_VIDEO_QUALITY,
SET_PASSWORD,
SET_PASSWORD_FAILED,
SET_RECEIVE_VIDEO_QUALITY,
SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
SET_ROOM,
SET_START_MUTED_POLICY
} from './actionTypes';
@@ -569,6 +570,23 @@ export function setLastN(lastN: ?number) {
};
}
/**
* Sets the max frame height that should be received from remote videos.
*
* @param {number} maxReceiverVideoQuality - The max video frame height to
* receive.
* @returns {{
* type: SET_MAX_RECEIVER_VIDEO_QUALITY,
* maxReceiverVideoQuality: number
* }}
*/
export function setMaxReceiverVideoQuality(maxReceiverVideoQuality: number) {
return {
type: SET_MAX_RECEIVER_VIDEO_QUALITY,
maxReceiverVideoQuality
};
}
/**
* Sets the password to join or lock a specific JitsiConference.
*
@@ -641,18 +659,21 @@ export function setPassword(
}
/**
* Sets the max frame height to receive from remote participant videos.
* Sets the max frame height the user prefers to receive from remote participant
* videos.
*
* @param {number} receiveVideoQuality - The max video resolution to receive.
* @param {number} preferredReceiverVideoQuality - The max video resolution to
* receive.
* @returns {{
* type: SET_RECEIVE_VIDEO_QUALITY,
* receiveVideoQuality: number
* type: SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
* preferredReceiverVideoQuality: number
* }}
*/
export function setReceiveVideoQuality(receiveVideoQuality: number) {
export function setPreferredReceiverVideoQuality(
preferredReceiverVideoQuality: number) {
return {
type: SET_RECEIVE_VIDEO_QUALITY,
receiveVideoQuality
type: SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
preferredReceiverVideoQuality
};
}

View File

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

View File

@@ -17,24 +17,24 @@ import {
getPinnedParticipant,
PIN_PARTICIPANT
} from '../participants';
import { MiddlewareRegistry } from '../redux';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import {
conferenceFailed,
conferenceLeft,
conferenceWillLeave,
createConference,
setLastN,
toggleAudioOnly
setLastN
} from './actions';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY,
SET_LASTN,
SET_RECEIVE_VIDEO_QUALITY,
SET_ROOM
} from './actionTypes';
import {
@@ -48,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.
*
@@ -68,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);
@@ -80,9 +89,6 @@ MiddlewareRegistry.register(store => next => action => {
case SET_LASTN:
return _setLastN(store, next, action);
case SET_RECEIVE_VIDEO_QUALITY:
return _setReceiveVideoQuality(store, next, action);
case SET_ROOM:
return _setRoom(store, next, action);
@@ -94,6 +100,32 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
/**
* Registers a change handler for state['features/base/conference'] to update
* the preferred video quality levels based on user preferred and internal
* settings.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/conference'],
/* listener */ (currentState, store, previousState = {}) => {
const {
conference,
maxReceiverVideoQuality,
preferredReceiverVideoQuality
} = currentState;
const changedPreferredVideoQuality = preferredReceiverVideoQuality
!== previousState.preferredReceiverVideoQuality;
const changedMaxVideoQuality = maxReceiverVideoQuality
!== previousState.maxReceiverVideoQuality;
if (changedPreferredVideoQuality || changedMaxVideoQuality) {
_setReceiverVideoConstraint(
conference,
preferredReceiverVideoQuality,
maxReceiverVideoQuality);
}
});
/**
* Makes sure to leave a failed conference in order to release any allocated
* resources like peer connections, emit participant left events, etc.
@@ -114,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;
}
@@ -154,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;
}
@@ -206,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.
@@ -245,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
@@ -426,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}`);
}
}
@@ -434,27 +501,20 @@ function _setLastN({ getState }, next, action) {
}
/**
* Sets the maximum receive video quality and will turn off audio only mode if
* enabled.
* Helper function for updating the preferred receiver video constraint, based
* on the user preference and the internal maximum.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} to the specified {@code store}.
* @param {Action} action - The redux action {@code SET_RECEIVE_VIDEO_QUALITY}
* which is being dispatched in the specified {@code store}.
* @private
* @returns {Object} The value returned by {@code next(action)}.
* @param {JitsiConference} conference - The JitsiConference instance for the
* current call.
* @param {number} preferred - The user preferred max frame height.
* @param {number} max - The maximum frame height the application should
* receive.
* @returns {void}
*/
function _setReceiveVideoQuality({ dispatch, getState }, next, action) {
const { audioOnly, conference } = getState()['features/base/conference'];
function _setReceiverVideoConstraint(conference, preferred, max) {
if (conference) {
conference.setReceiverVideoConstraint(action.receiveVideoQuality);
audioOnly && dispatch(toggleAudioOnly());
conference.setReceiverVideoConstraint(Math.min(preferred, max));
}
return next(action);
}
/**
@@ -544,9 +604,16 @@ function _syncConferenceLocalTracksWithState({ getState }, action) {
* @returns {Object} The value returned by {@code next(action)}.
*/
function _syncReceiveVideoQuality({ getState }, next, action) {
const state = getState()['features/base/conference'];
const {
conference,
maxReceiverVideoQuality,
preferredReceiverVideoQuality
} = getState()['features/base/conference'];
state.conference.setReceiverVideoConstraint(state.receiveVideoQuality);
_setReceiverVideoConstraint(
conference,
preferredReceiverVideoQuality,
maxReceiverVideoQuality);
return next(action);
}

View File

@@ -17,8 +17,9 @@ import {
SET_AUDIO_ONLY,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_MAX_RECEIVER_VIDEO_QUALITY,
SET_PASSWORD,
SET_RECEIVE_VIDEO_QUALITY,
SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
SET_ROOM,
SET_SIP_GATEWAY_ENABLED,
SET_START_MUTED_POLICY
@@ -26,71 +27,88 @@ import {
import { VIDEO_QUALITY_LEVELS } from './constants';
import { isRoomValid } from './functions';
const DEFAULT_STATE = {
joining: undefined,
maxReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH,
preferredReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
};
/**
* Listen for actions that contain the conference object, so that it can be
* stored for use by other action creators.
*/
ReducerRegistry.register('features/base/conference', (state = {}, action) => {
switch (action.type) {
case AUTH_STATUS_CHANGED:
return _authStatusChanged(state, action);
ReducerRegistry.register(
'features/base/conference',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case AUTH_STATUS_CHANGED:
return _authStatusChanged(state, action);
case CONFERENCE_FAILED:
return _conferenceFailed(state, action);
case CONFERENCE_FAILED:
return _conferenceFailed(state, action);
case CONFERENCE_JOINED:
return _conferenceJoined(state, action);
case CONFERENCE_JOINED:
return _conferenceJoined(state, action);
case CONFERENCE_LEFT:
case CONFERENCE_WILL_LEAVE:
return _conferenceLeftOrWillLeave(state, action);
case CONFERENCE_LEFT:
case CONFERENCE_WILL_LEAVE:
return _conferenceLeftOrWillLeave(state, action);
case CONFERENCE_WILL_JOIN:
return _conferenceWillJoin(state, action);
case CONFERENCE_WILL_JOIN:
return _conferenceWillJoin(state, action);
case CONNECTION_WILL_CONNECT:
return set(state, 'authRequired', undefined);
case CONNECTION_WILL_CONNECT:
return set(state, 'authRequired', undefined);
case LOCK_STATE_CHANGED:
return _lockStateChanged(state, action);
case LOCK_STATE_CHANGED:
return _lockStateChanged(state, action);
case P2P_STATUS_CHANGED:
return _p2pStatusChanged(state, action);
case P2P_STATUS_CHANGED:
return _p2pStatusChanged(state, action);
case SET_AUDIO_ONLY:
return _setAudioOnly(state, action);
case SET_AUDIO_ONLY:
return _setAudioOnly(state, action);
case SET_DESKTOP_SHARING_ENABLED:
return _setDesktopSharingEnabled(state, action);
case SET_DESKTOP_SHARING_ENABLED:
return _setDesktopSharingEnabled(state, action);
case SET_FOLLOW_ME:
return set(state, 'followMeEnabled', action.enabled);
case SET_FOLLOW_ME:
return set(state, 'followMeEnabled', action.enabled);
case SET_LOCATION_URL:
return set(state, 'room', undefined);
case SET_LOCATION_URL:
return set(state, 'room', undefined);
case SET_PASSWORD:
return _setPassword(state, action);
case SET_MAX_RECEIVER_VIDEO_QUALITY:
return set(
state,
'maxReceiverVideoQuality',
action.maxReceiverVideoQuality);
case SET_RECEIVE_VIDEO_QUALITY:
return _setReceiveVideoQuality(state, action);
case SET_PASSWORD:
return _setPassword(state, action);
case SET_ROOM:
return _setRoom(state, action);
case SET_PREFERRED_RECEIVER_VIDEO_QUALITY:
return set(
state,
'preferredReceiverVideoQuality',
action.preferredReceiverVideoQuality);
case SET_SIP_GATEWAY_ENABLED:
return _setSIPGatewayEnabled(state, action);
case SET_ROOM:
return _setRoom(state, action);
case SET_START_MUTED_POLICY:
return {
...state,
startAudioMutedPolicy: action.startAudioMutedPolicy,
startVideoMutedPolicy: action.startVideoMutedPolicy
};
}
case SET_SIP_GATEWAY_ENABLED:
return _setSIPGatewayEnabled(state, action);
return state;
});
case SET_START_MUTED_POLICY:
return {
...state,
startAudioMutedPolicy: action.startAudioMutedPolicy,
startVideoMutedPolicy: action.startVideoMutedPolicy
};
}
return state;
});
/**
* Reduces a specific Redux action AUTH_STATUS_CHANGED of the feature
@@ -203,15 +221,7 @@ function _conferenceJoined(state, { conference }) {
* @type {boolean}
*/
locked,
passwordRequired: undefined,
/**
* The current resolution restraint on receiving remote video. By
* default the conference will send the highest level possible.
*
* @type number
*/
receiveVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
passwordRequired: undefined
});
}
@@ -402,21 +412,6 @@ function _setPassword(state, { conference, method, password }) {
return state;
}
/**
* Reduces a specific Redux action SET_RECEIVE_VIDEO_QUALITY of the feature
* base/conference.
*
* @param {Object} state - The Redux state of the feature base/conference.
* @param {Action} action - The Redux action SET_RECEIVE_VIDEO_QUALITY to
* reduce.
* @private
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _setReceiveVideoQuality(state, action) {
return set(state, 'receiveVideoQuality', action.receiveVideoQuality);
}
/**
* Reduces a specific Redux action SET_ROOM of the feature base/conference.
*

View File

@@ -1,5 +1,7 @@
/* @flow */
import { reportError } from '../util';
/**
* Parses the query/search or fragment/hash parameters out of a specific URL and
* returns them as a JS object.
@@ -36,10 +38,8 @@ export default function parseURLParams(
= JSON.parse(decodeURIComponent(value).replace(/\\&/, '&'));
}
} catch (e) {
const msg = `Failed to parse URL parameter value: ${String(value)}`;
console.warn(msg, e);
window.onerror && window.onerror(msg, null, null, null, e);
reportError(
e, `Failed to parse URL parameter value: ${String(value)}`);
return;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -98,3 +98,25 @@ export const PARTICIPANT_UPDATED = Symbol('PARTICIPANT_UPDATED');
* }
*/
export const PIN_PARTICIPANT = Symbol('PIN_PARTICIPANT');
/**
* Action to signal that a hidden participant has joined.
*
* {
* type: HIDDEN_PARTICIPANT_JOINED,
* participant: Participant
* }
*/
export const HIDDEN_PARTICIPANT_JOINED = Symbol('HIDDEN_PARTICIPANT_JOINED');
/**
* Action to handle case when hidden participant leaves.
*
* {
* type: PARTICIPANT_LEFT,
* participant: {
* id: string
* }
* }
*/
export const HIDDEN_PARTICIPANT_LEFT = Symbol('HIDDEN_PARTICIPANT_LEFT');

View File

@@ -7,6 +7,8 @@ import { showNotification } from '../../notifications';
import {
DOMINANT_SPEAKER_CHANGED,
HIDDEN_PARTICIPANT_JOINED,
HIDDEN_PARTICIPANT_LEFT,
KICK_PARTICIPANT,
MUTE_REMOTE_PARTICIPANT,
PARTICIPANT_DISPLAY_NAME_CHANGED,
@@ -276,6 +278,42 @@ export function participantJoined(participant) {
};
}
/**
* Action to signal that a hidden participant has joined the conference.
*
* @param {string} id - The id of the participant.
* @param {string} displayName - The display name, or undefined when
* unknown.
* @returns {{
* type: HIDDEN_PARTICIPANT_JOINED,
* displayName: string,
* id: string
* }}
*/
export function hiddenParticipantJoined(id, displayName) {
return {
type: HIDDEN_PARTICIPANT_JOINED,
id,
displayName
};
}
/**
* Action to signal that a hidden participant has left the conference.
*
* @param {string} id - The id of the participant.
* @returns {{
* type: HIDDEN_PARTICIPANT_LEFT,
* id: string
* }}
*/
export function hiddenParticipantLeft(id) {
return {
type: HIDDEN_PARTICIPANT_LEFT,
id
};
}
/**
* Action to signal that a participant has left.
*

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import React, { Component } from 'react';
*/
const DIALOG_TO_PADDING_POSITION = {
'left': 'popover-mousemove-padding-right',
'right': 'popover-mousemove-padding-left',
'top': 'popover-mousemove-padding-bottom'
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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