mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-24 22:17:47 +00:00
Compare commits
58 Commits
2948
...
base_sessi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2191e3a28 | ||
|
|
72e3e8593d | ||
|
|
67a8b4915d | ||
|
|
468d4a7150 | ||
|
|
2a01e29fec | ||
|
|
90a64d30dc | ||
|
|
31905d4f63 | ||
|
|
7e1d97665a | ||
|
|
b978851a0f | ||
|
|
ef49817eaf | ||
|
|
cac8888b37 | ||
|
|
81853d971a | ||
|
|
b9c5ed3b03 | ||
|
|
0892e0b644 | ||
|
|
b41bf22be7 | ||
|
|
a1cc9bce91 | ||
|
|
8d3cecad86 | ||
|
|
bd8559fad6 | ||
|
|
fb75180632 | ||
|
|
046b06e436 | ||
|
|
af7c69a1aa | ||
|
|
7ad0639f7a | ||
|
|
54a1853e60 | ||
|
|
27021ea271 | ||
|
|
f5a667ad9e | ||
|
|
2b9ce40533 | ||
|
|
d3dd833f21 | ||
|
|
1cc372868b | ||
|
|
a6956c7c34 | ||
|
|
aaaa3e05d1 | ||
|
|
467a5aaae3 | ||
|
|
243dd16285 | ||
|
|
92001f4d37 | ||
|
|
6a31c59081 | ||
|
|
11c5b220a1 | ||
|
|
590ad90cd1 | ||
|
|
ca62e902bc | ||
|
|
34d1eb6768 | ||
|
|
b6b21e5410 | ||
|
|
b8daf0a9f9 | ||
|
|
39f1958300 | ||
|
|
0b1224495b | ||
|
|
ee7d180cbb | ||
|
|
4d3383c620 | ||
|
|
fd78203ff8 | ||
|
|
a36b341865 | ||
|
|
3d6e18394e | ||
|
|
9a6e5c67f5 | ||
|
|
50ea847905 | ||
|
|
b54a9e2bf7 | ||
|
|
918fb1dfc6 | ||
|
|
9f015df61d | ||
|
|
2cd1b7f80b | ||
|
|
afd2aea79c | ||
|
|
c62f761d67 | ||
|
|
acda279111 | ||
|
|
ccf9e2a362 | ||
|
|
3154c6f936 |
@@ -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/.*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
osx_image: xcode9.3
|
||||
osx_image: xcode9.4
|
||||
language: objective-c
|
||||
script:
|
||||
- "./ios/travis-ci/build-ipa.sh"
|
||||
|
||||
6
android/app/proguard-rules.pro
vendored
6
android/app/proguard-rules.pro
vendored
@@ -68,3 +68,9 @@
|
||||
-dontwarn java.nio.file.*
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-dontwarn okio.**
|
||||
|
||||
# FastImage
|
||||
|
||||
-keep public class com.dylanvann.fastimage.* {*;}
|
||||
-keep public class com.dylanvann.fastimage.** {*;}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "$rootDir/../node_modules/jsc-android/dist" }
|
||||
// React Native (JS, Obj-C sources, Android binaries) is installed from
|
||||
// npm.
|
||||
maven { url "$rootDir/../node_modules/react-native/android" }
|
||||
@@ -34,6 +35,12 @@ allprojects {
|
||||
def version = new groovy.json.JsonSlurper().parseText(file.text).version
|
||||
details.useVersion version
|
||||
}
|
||||
if (details.requested.group == 'org.webkit'
|
||||
&& details.requested.name == 'android-jsc') {
|
||||
def file = new File("$rootDir/../node_modules/jsc-android/package.json")
|
||||
def version = new groovy.json.JsonSlurper().parseText(file.text).version
|
||||
details.useVersion "r${version.tokenize('.')[0]}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +153,7 @@ allprojects {
|
||||
ext {
|
||||
buildToolsVersion = "26.0.2"
|
||||
compileSdkVersion = 26
|
||||
minSdkVersion = 16
|
||||
minSdkVersion = 21
|
||||
targetSdkVersion = 26
|
||||
|
||||
// The Maven artifact groupdId of the third-party react-native modules which
|
||||
|
||||
@@ -25,7 +25,7 @@ dependencies {
|
||||
compile 'com.facebook.react:react-native:+'
|
||||
|
||||
compile project(':react-native-background-timer')
|
||||
compile project(':react-native-fetch-blob')
|
||||
compile project(':react-native-fast-image')
|
||||
compile project(':react-native-immersive')
|
||||
compile project(':react-native-keep-awake')
|
||||
compile project(':react-native-linear-gradient')
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
109
conference.js
109
conference.js
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
43
css/_navigate_section_list.scss
Normal file
43
css/_navigate_section_list.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
%navigate-section-list-text {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: $welcomePageTitleColor;
|
||||
text-align: left;
|
||||
font-family: 'open_sanslight', Helvetica, sans-serif;
|
||||
}
|
||||
%navigate-section-list-tile-text {
|
||||
@extend %navigate-section-list-text;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
float: left;
|
||||
}
|
||||
.navigate-section-list-tile {
|
||||
height: 90px;
|
||||
width: 260px;
|
||||
border-radius: 4px;
|
||||
background-color: #1754A9;
|
||||
margin-right: 8px;
|
||||
padding: 16px;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
.navigate-section-tile-body {
|
||||
@extend %navigate-section-list-tile-text;
|
||||
font-weight: normal;
|
||||
}
|
||||
.navigate-section-tile-title {
|
||||
@extend %navigate-section-list-tile-text;
|
||||
font-weight: bold;
|
||||
}
|
||||
.navigate-section-section-header {
|
||||
@extend %navigate-section-list-text;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.navigate-section-list {
|
||||
position: relative;
|
||||
margin-top: 36px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,9 @@
|
||||
object-fit: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.presence-label {
|
||||
position: absolute;
|
||||
z-index: $zindex3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -78,4 +78,5 @@
|
||||
@import 'modals/invite/add-people';
|
||||
@import 'deep-linking/main';
|
||||
@import 'transcription-subtitles';
|
||||
@import 'navigate_section_list';
|
||||
/* Modules END */
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,6 @@
|
||||
|
||||
.circular-label {
|
||||
color: white;
|
||||
font-family: -apple-system, BlinkMacSystemFont, $baseFontFamily;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
opacity: 0.8;
|
||||
|
||||
@@ -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.
|
||||
|
||||
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
@@ -72,4 +72,7 @@
|
||||
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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 |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
1201
fonts/selection.json
1201
fonts/selection.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB |
@@ -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';
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -130,6 +130,7 @@ cp "${CERT_DIR}/dev-profile.mobileprovision" ~/Library/MobileDevice/Provisionin
|
||||
npm install
|
||||
|
||||
cd ios
|
||||
pod update
|
||||
pod install
|
||||
cd ..
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable no-unused-vars, no-var */
|
||||
|
||||
// Logging configuration
|
||||
// XXX When making any changes to this file make sure to also update it's React
|
||||
// version at ./react/features/base/logging/reducer.js !!!
|
||||
var loggingConfig = {
|
||||
// default log level for the app and lib-jitsi-meet
|
||||
defaultLogLevel: 'trace',
|
||||
@@ -11,9 +10,17 @@ var loggingConfig = {
|
||||
|
||||
// The following are too verbose in their logging with the
|
||||
// {@link #defaultLogLevel}:
|
||||
'modules/RTC/TraceablePeerConnection.js': 'info',
|
||||
'modules/statistics/CallStats.js': 'info',
|
||||
'modules/xmpp/strophe.util.js': 'log',
|
||||
'modules/RTC/TraceablePeerConnection.js': 'info'
|
||||
'modules/xmpp/strophe.util.js': 'log'
|
||||
};
|
||||
|
||||
/* eslint-enable no-unused-vars, no-var */
|
||||
|
||||
// XXX Web/React server-includes logging_config.js into index.html.
|
||||
// Mobile/react-native requires it in react/features/base/logging. For the
|
||||
// purposes of the latter, (try to) export loggingConfig. The following
|
||||
// detection of a module system is inspired by webpack.
|
||||
typeof module === 'object'
|
||||
&& typeof exports === 'object'
|
||||
&& (module.exports = loggingConfig);
|
||||
|
||||
@@ -112,11 +112,21 @@ function initCommands() {
|
||||
const { name } = request;
|
||||
|
||||
switch (name) {
|
||||
case 'invite':
|
||||
case 'invite': // eslint-disable-line no-case-declarations
|
||||
const { invitees } = request;
|
||||
|
||||
if (!Array.isArray(invitees) || invitees.length === 0) {
|
||||
callback({
|
||||
error: new Error('Unexpected format of invitees')
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// The store should be already available because API.init is called
|
||||
// on appWillMount action.
|
||||
APP.store.dispatch(
|
||||
invite(request.invitees, true))
|
||||
invite(invitees, true))
|
||||
.then(failedInvitees => {
|
||||
let error;
|
||||
let result;
|
||||
|
||||
8
modules/API/external/external_api.js
vendored
8
modules/API/external/external_api.js
vendored
@@ -238,7 +238,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
}
|
||||
})
|
||||
});
|
||||
this.invite(invitees);
|
||||
if (Array.isArray(invitees) && invitees.length > 0) {
|
||||
this.invite(invitees);
|
||||
}
|
||||
this._isLargeVideoVisible = true;
|
||||
this._numberOfParticipants = 0;
|
||||
this._participants = {};
|
||||
@@ -597,6 +599,10 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* @returns {Promise} - Resolves on success and rejects on failure.
|
||||
*/
|
||||
invite(invitees) {
|
||||
if (!Array.isArray(invitees) || invitees.length === 0) {
|
||||
return Promise.reject(new TypeError('Invalid Argument'));
|
||||
}
|
||||
|
||||
return this._transport.sendRequest({
|
||||
name: 'invite',
|
||||
invitees
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
86
package-lock.json
generated
@@ -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",
|
||||
|
||||
14
package.json
14
package.json
@@ -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"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AbstractAudioMuteButton } from '../base/toolbox';
|
||||
import type { AbstractButtonProps as Props } from '../base/toolbox';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link AudioMuteButton}.
|
||||
@@ -68,7 +69,7 @@ export default class AudioMuteButton
|
||||
audioAvailable,
|
||||
audioMuted
|
||||
}))
|
||||
.catch(console.error);
|
||||
.catch(logger.error);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AbstractVideoMuteButton } from '../base/toolbox';
|
||||
import type { AbstractButtonProps as Props } from '../base/toolbox';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link VideoMuteButton}.
|
||||
@@ -68,7 +69,7 @@ export default class VideoMuteButton
|
||||
videoAvailable,
|
||||
videoMuted
|
||||
}))
|
||||
.catch(console.error);
|
||||
.catch(logger.error);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
AspectRatioDetector,
|
||||
ReducedUIDetector
|
||||
} from '../../base/responsive-ui';
|
||||
import '../../google-api';
|
||||
import '../../mobile/audio-mode';
|
||||
import '../../mobile/background';
|
||||
import '../../mobile/callkit';
|
||||
@@ -26,6 +27,8 @@ import type { Props as AbstractAppProps } from './AbstractApp';
|
||||
|
||||
declare var __DEV__;
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The type of React {@code Component} props of {@link App}.
|
||||
*/
|
||||
@@ -196,7 +199,7 @@ function _handleException(error, fatal) {
|
||||
// an unhandled JavascriptException for an unhandled JavaScript error.
|
||||
// This will effectively kill the app. In accord with the Web, do not
|
||||
// kill the app.
|
||||
console.error(error);
|
||||
logger.error(error);
|
||||
} else {
|
||||
// Forward to the next globalHandler of ErrorUtils.
|
||||
const { next } = _handleException;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ const INITIAL_RN_STATE = {
|
||||
// fastest to merely disable them.
|
||||
disableAudioLevels: true,
|
||||
|
||||
// FIXME flow complains about missing 'locationURL' missing in _setConfig
|
||||
locationURL: undefined,
|
||||
|
||||
p2p: {
|
||||
disableH264: false,
|
||||
preferH264: true
|
||||
@@ -126,8 +129,10 @@ function _setConfig(state, { config }) {
|
||||
|
||||
const newState = _.merge(
|
||||
{},
|
||||
config,
|
||||
{ error: undefined },
|
||||
config, {
|
||||
error: undefined,
|
||||
locationURL: state.locationURL
|
||||
},
|
||||
|
||||
// The config of _getInitialState() is meant to override the config
|
||||
// downloaded from the Jitsi Meet deployment because the former contains
|
||||
|
||||
@@ -9,17 +9,6 @@
|
||||
*/
|
||||
export const SET_AUDIO_INPUT_DEVICE = Symbol('SET_AUDIO_INPUT_DEVICE');
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the currently used audio
|
||||
* output device should be changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_AUDIO_OUTPUT_DEVICE,
|
||||
* deviceId: string,
|
||||
* }
|
||||
*/
|
||||
export const SET_AUDIO_OUTPUT_DEVICE = Symbol('SET_AUDIO_OUTPUT_DEVICE');
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the currently used video
|
||||
* input device should be changed.
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
|
||||
import {
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_AUDIO_OUTPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Queries for connected A/V input and output devices and updates the redux
|
||||
* state of known devices.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function getAvailableDevices() {
|
||||
return dispatch => new Promise(resolve => {
|
||||
const { mediaDevices } = JitsiMeetJS;
|
||||
|
||||
if (mediaDevices.isDeviceListAvailable()
|
||||
&& mediaDevices.isDeviceChangeAvailable()) {
|
||||
mediaDevices.enumerateDevices(devices => {
|
||||
dispatch(updateDeviceList(devices));
|
||||
|
||||
resolve(devices);
|
||||
});
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used audio input device.
|
||||
*
|
||||
@@ -21,22 +45,6 @@ export function setAudioInputDevice(deviceId) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used audio output device.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new audio ouput device.
|
||||
* @returns {{
|
||||
* type: SET_AUDIO_OUTPUT_DEVICE,
|
||||
* deviceId: string
|
||||
* }}
|
||||
*/
|
||||
export function setAudioOutputDevice(deviceId) {
|
||||
return {
|
||||
type: SET_AUDIO_OUTPUT_DEVICE,
|
||||
deviceId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used video input device.
|
||||
*
|
||||
|
||||
@@ -6,7 +6,6 @@ import { MiddlewareRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_AUDIO_OUTPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE
|
||||
} from './actionTypes';
|
||||
|
||||
@@ -22,9 +21,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case SET_AUDIO_INPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
|
||||
break;
|
||||
case SET_AUDIO_OUTPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, action.deviceId);
|
||||
break;
|
||||
case SET_VIDEO_INPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_AUDIO_OUTPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
@@ -40,7 +39,6 @@ ReducerRegistry.register(
|
||||
// now.
|
||||
case SET_AUDIO_INPUT_DEVICE:
|
||||
case SET_VIDEO_INPUT_DEVICE:
|
||||
case SET_AUDIO_OUTPUT_DEVICE:
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,28 @@ class DialogWithTabs extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the props to pass into the tab component.
|
||||
*
|
||||
* @param {number} tabId - The index of the tab configuration within
|
||||
* {@link this.state.tabStates}.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getTabProps(tabId) {
|
||||
const { tabs } = this.props;
|
||||
const { tabStates } = this.state;
|
||||
const tabConfiguration = tabs[tabId];
|
||||
const currentTabState = tabStates[tabId];
|
||||
|
||||
if (tabConfiguration.propsUpdateFunction) {
|
||||
return tabConfiguration.propsUpdateFunction(
|
||||
currentTabState,
|
||||
tabConfiguration.props);
|
||||
}
|
||||
|
||||
return { ...currentTabState };
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the tabs from the tab information passed on props.
|
||||
*
|
||||
@@ -155,10 +177,11 @@ class DialogWithTabs extends Component<Props, State> {
|
||||
<div className = { styles }>
|
||||
<TabComponent
|
||||
closeDialog = { closeDialog }
|
||||
mountCallback = { this.props.tabs[tabId].onMount }
|
||||
onTabStateChange
|
||||
= { this._onTabStateChange }
|
||||
tabId = { tabId }
|
||||
{ ...this.state.tabStates[tabId] } />
|
||||
{ ...this._getTabProps(tabId) } />
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,9 @@ import moment from 'moment';
|
||||
|
||||
import i18next from './i18next';
|
||||
|
||||
// allows for moment durations to be formatted
|
||||
import 'moment-duration-format';
|
||||
|
||||
// MomentJS uses static language bundle loading, so in order to support dynamic
|
||||
// language selection in the app we need to load all bundles that we support in
|
||||
// the app.
|
||||
@@ -55,8 +58,19 @@ export function getLocalizedDurationFormatter(duration: number) {
|
||||
// states v2.19 so maybe locale on moment's duration was introduced in
|
||||
// between?
|
||||
//
|
||||
|
||||
// If the conference is under an hour long we want to display it without
|
||||
// showing the hour and we want to include the hour if the conference is
|
||||
// more than an hour long
|
||||
|
||||
// $FlowFixMe
|
||||
return moment.duration(duration).locale(_getSupportedLocale());
|
||||
if (moment.duration(duration).format('h') !== '0') {
|
||||
// $FlowFixMe
|
||||
return moment.duration(duration).format('h:mm:ss');
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
return moment.duration(duration).format('mm:ss', { trim: false });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,8 @@ declare var APP: Object;
|
||||
const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
|
||||
const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Creates a {@link JitsiLocalTrack} model from the given device id.
|
||||
*
|
||||
@@ -133,7 +135,7 @@ export function loadConfig(
|
||||
return config;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`Failed to load config from ${url}`, err);
|
||||
logger.error(`Failed to load config from ${url}`, err);
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { NativeModules } from 'react-native';
|
||||
import { RTCPeerConnection, RTCSessionDescription } from 'react-native-webrtc';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
// Address families.
|
||||
@@ -140,7 +142,7 @@ _RTCPeerConnection.prototype.setRemoteDescription = function(
|
||||
* @returns {void}
|
||||
*/
|
||||
function _LOGE(...args) {
|
||||
console && console.error && console.error(...args);
|
||||
logger.error(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
75
react/features/base/react/Types.js
Normal file
75
react/features/base/react/Types.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// @flow
|
||||
|
||||
import type { ComponentType, Element } from 'react';
|
||||
|
||||
/**
|
||||
* Item data for <tt>NavigateSectionList</tt>.
|
||||
*/
|
||||
export type Item = {
|
||||
|
||||
/**
|
||||
* the color base of the avatar
|
||||
*/
|
||||
colorBase: string,
|
||||
|
||||
/**
|
||||
* Item title
|
||||
*/
|
||||
title: string,
|
||||
|
||||
/**
|
||||
* Item url
|
||||
*/
|
||||
url: string,
|
||||
|
||||
/**
|
||||
* lines[0] - date
|
||||
* lines[1] - duration
|
||||
* lines[2] - server name
|
||||
*/
|
||||
lines: Array<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* web implementation of section data for NavigateSectionList
|
||||
*/
|
||||
export type Section = {
|
||||
|
||||
/**
|
||||
* section title
|
||||
*/
|
||||
title: string,
|
||||
|
||||
/**
|
||||
* unique key for the section
|
||||
*/
|
||||
key?: string,
|
||||
|
||||
/**
|
||||
* Array of items in the section
|
||||
*/
|
||||
data: $ReadOnlyArray<Item>,
|
||||
|
||||
/**
|
||||
* Optional properties added only to fix some flow errors thrown by React
|
||||
* SectionList
|
||||
*/
|
||||
ItemSeparatorComponent?: ?ComponentType<any>,
|
||||
|
||||
keyExtractor?: (item: Object) => string,
|
||||
|
||||
renderItem?: ?(info: Object) => ?Element<any>
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* native implementation of section data for NavigateSectionList
|
||||
*
|
||||
* When react-native's SectionList component parses through an array of sections
|
||||
* it passes the section nested within the section property of another object
|
||||
* to the renderSection method (on web for our own implementation of SectionList
|
||||
* this nesting is not implemented as there is no need for nesting)
|
||||
*/
|
||||
export type SetionListSection = {
|
||||
section: Section
|
||||
}
|
||||
223
react/features/base/react/components/NavigateSectionList.js
Normal file
223
react/features/base/react/components/NavigateSectionList.js
Normal file
@@ -0,0 +1,223 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// TODO: Maybe try to make all NavigateSectionList components to work for both
|
||||
// mobile and web, and move them to NavigateSectionList component.
|
||||
import {
|
||||
NavigateSectionListEmptyComponent,
|
||||
NavigateSectionListItem,
|
||||
NavigateSectionListSectionHeader,
|
||||
SectionList
|
||||
} from './_';
|
||||
import type { Section } from '../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* Function to be invoked when an item is pressed. The item's URL is passed.
|
||||
*/
|
||||
onPress: Function,
|
||||
|
||||
/**
|
||||
* Function to be invoked when pull-to-refresh is performed.
|
||||
*/
|
||||
onRefresh: Function,
|
||||
|
||||
/**
|
||||
* Function to override the rendered default empty list component.
|
||||
*/
|
||||
renderListEmptyComponent: Function,
|
||||
|
||||
/**
|
||||
* An array of sections
|
||||
*/
|
||||
sections: Array<Section>
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a general section list to display items that have a URL property
|
||||
* and navigates to (probably) meetings, such as the recent list or the meeting
|
||||
* list components.
|
||||
*/
|
||||
class NavigateSectionList extends Component<Props> {
|
||||
/**
|
||||
* Creates an empty section object.
|
||||
*
|
||||
* @param {string} title - The title of the section.
|
||||
* @param {string} key - The key of the section. It must be unique.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
static createSection(title: string, key: string) {
|
||||
return {
|
||||
data: [],
|
||||
key,
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor of the NavigateSectionList component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this._getItemKey = this._getItemKey.bind(this);
|
||||
this._onPress = this._onPress.bind(this);
|
||||
this._onRefresh = this._onRefresh.bind(this);
|
||||
this._renderItem = this._renderItem.bind(this);
|
||||
this._renderListEmptyComponent
|
||||
= this._renderListEmptyComponent.bind(this);
|
||||
this._renderSectionHeader = this._renderSectionHeader.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's Component.render.
|
||||
* Note: we don't use the refreshing value yet, because refreshing of these
|
||||
* lists is super quick, no need to complicate the code - yet.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
renderListEmptyComponent = this._renderListEmptyComponent,
|
||||
sections
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
ListEmptyComponent = { renderListEmptyComponent }
|
||||
keyExtractor = { this._getItemKey }
|
||||
onItemClick = { this.props.onPress }
|
||||
onRefresh = { this._onRefresh }
|
||||
refreshing = { false }
|
||||
renderItem = { this._renderItem }
|
||||
renderSectionHeader = { this._renderSectionHeader }
|
||||
sections = { sections } />
|
||||
);
|
||||
}
|
||||
|
||||
_getItemKey: (Object, number) => string;
|
||||
|
||||
/**
|
||||
* Generates a unique id to every item.
|
||||
*
|
||||
* @param {Object} item - The item.
|
||||
* @param {number} index - The item index.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getItemKey(item, index) {
|
||||
return `${index}-${item.key}`;
|
||||
}
|
||||
|
||||
_onPress: string => Function;
|
||||
|
||||
/**
|
||||
* Returns a function that is used in the onPress callback of the items.
|
||||
*
|
||||
* @param {string} url - The URL of the item to navigate to.
|
||||
* @private
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onPress(url) {
|
||||
return () => {
|
||||
const { disabled, onPress } = this.props;
|
||||
|
||||
!disabled && url && typeof onPress === 'function' && onPress(url);
|
||||
};
|
||||
}
|
||||
|
||||
_onRefresh: () => void;
|
||||
|
||||
/**
|
||||
* Invokes the onRefresh callback if present.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRefresh() {
|
||||
const { onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
onRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
_renderItem: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a single item in the list.
|
||||
*
|
||||
* @param {Object} listItem - The item to render.
|
||||
* @param {string} key - The item needed for rendering using map on web.
|
||||
* @private
|
||||
* @returns {Component}
|
||||
*/
|
||||
_renderItem(listItem, key: string = '') {
|
||||
const { item } = listItem;
|
||||
const { url } = item;
|
||||
|
||||
// XXX The value of title cannot be undefined; otherwise, react-native
|
||||
// will throw a TypeError: Cannot read property of undefined. While it's
|
||||
// difficult to get an undefined title and very likely requires the
|
||||
// execution of incorrect source code, it is undesirable to break the
|
||||
// whole app because of an undefined string.
|
||||
if (typeof item.title === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigateSectionListItem
|
||||
item = { item }
|
||||
key = { key }
|
||||
onPress = { this._onPress(url) } />
|
||||
);
|
||||
}
|
||||
|
||||
_renderListEmptyComponent: () => Object;
|
||||
|
||||
/**
|
||||
* Renders a component to display when the list is empty.
|
||||
*
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderListEmptyComponent() {
|
||||
const { onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
return (
|
||||
<NavigateSectionListEmptyComponent />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_renderSectionHeader: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a section header.
|
||||
*
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderSectionHeader(section) {
|
||||
return (
|
||||
<NavigateSectionListSectionHeader
|
||||
section = { section } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NavigateSectionList;
|
||||
@@ -1 +1,2 @@
|
||||
export * from './_';
|
||||
export { default as NavigateSectionList } from './NavigateSectionList';
|
||||
|
||||
@@ -34,6 +34,7 @@ export default class Container extends AbstractContainer {
|
||||
accessible,
|
||||
onClick,
|
||||
touchFeedback = onClick,
|
||||
underlayColor,
|
||||
visible = true,
|
||||
...props
|
||||
} = this.props;
|
||||
@@ -62,7 +63,8 @@ export default class Container extends AbstractContainer {
|
||||
{
|
||||
accessibilityLabel,
|
||||
accessible,
|
||||
onPress: onClick
|
||||
onPress: onClick,
|
||||
underlayColor
|
||||
},
|
||||
element);
|
||||
}
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
SafeAreaView,
|
||||
SectionList,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { Icon } from '../../../font-icons';
|
||||
import { translate } from '../../../i18n';
|
||||
|
||||
import styles, { UNDERLAY_COLOR } from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Function to be invoked when an item is pressed. The item's URL is passed.
|
||||
*/
|
||||
onPress: Function,
|
||||
|
||||
/**
|
||||
* Function to be invoked when pull-to-refresh is performed.
|
||||
*/
|
||||
onRefresh: Function,
|
||||
|
||||
/**
|
||||
* Function to override the rendered default empty list component.
|
||||
*/
|
||||
renderListEmptyComponent: Function,
|
||||
|
||||
/**
|
||||
* Sections to be rendered in the following format:
|
||||
*
|
||||
* [
|
||||
* {
|
||||
* title: string, <- section title
|
||||
* key: string, <- unique key for the section
|
||||
* data: [ <- Array of items in the section
|
||||
* {
|
||||
* colorBase: string, <- the color base of the avatar
|
||||
* title: string, <- item title
|
||||
* url: string, <- item url
|
||||
* lines: Array<string> <- additional lines to be rendered
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
sections: Array<Object>
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a general section list to display items that have a URL property
|
||||
* and navigates to (probably) meetings, such as the recent list or the meeting
|
||||
* list components.
|
||||
*/
|
||||
class NavigateSectionList extends Component<Props> {
|
||||
/**
|
||||
* Creates an empty section object.
|
||||
*
|
||||
* @param {string} title - The title of the section.
|
||||
* @param {string} key - The key of the section. It must be unique.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
static createSection(title, key) {
|
||||
return {
|
||||
data: [],
|
||||
key,
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor of the NavigateSectionList component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._getAvatarColor = this._getAvatarColor.bind(this);
|
||||
this._getItemKey = this._getItemKey.bind(this);
|
||||
this._onPress = this._onPress.bind(this);
|
||||
this._onRefresh = this._onRefresh.bind(this);
|
||||
this._renderItem = this._renderItem.bind(this);
|
||||
this._renderItemLine = this._renderItemLine.bind(this);
|
||||
this._renderItemLines = this._renderItemLines.bind(this);
|
||||
this._renderListEmptyComponent
|
||||
= this._renderListEmptyComponent.bind(this);
|
||||
this._renderSection = this._renderSection.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's Component.render.
|
||||
* Note: we don't use the refreshing value yet, because refreshing of these
|
||||
* lists is super quick, no need to complicate the code - yet.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
renderListEmptyComponent = this._renderListEmptyComponent,
|
||||
sections
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style = { styles.container } >
|
||||
<SectionList
|
||||
ListEmptyComponent = { renderListEmptyComponent }
|
||||
keyExtractor = { this._getItemKey }
|
||||
onRefresh = { this._onRefresh }
|
||||
refreshing = { false }
|
||||
renderItem = { this._renderItem }
|
||||
renderSectionHeader = { this._renderSection }
|
||||
sections = { sections }
|
||||
style = { styles.list } />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
_getAvatarColor: string => Object;
|
||||
|
||||
/**
|
||||
* Returns a style (color) based on the string that determines the color of
|
||||
* the avatar.
|
||||
*
|
||||
* @param {string} colorBase - The string that is the base of the color.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getAvatarColor(colorBase) {
|
||||
if (!colorBase) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nameHash = 0;
|
||||
|
||||
for (let i = 0; i < colorBase.length; i++) {
|
||||
nameHash += colorBase.codePointAt(i);
|
||||
}
|
||||
|
||||
return styles[`avatarColor${(nameHash % 5) + 1}`];
|
||||
}
|
||||
|
||||
_getItemKey: (Object, number) => string;
|
||||
|
||||
/**
|
||||
* Generates a unique id to every item.
|
||||
*
|
||||
* @param {Object} item - The item.
|
||||
* @param {number} index - The item index.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getItemKey(item, index) {
|
||||
return `${index}-${item.key}`;
|
||||
}
|
||||
|
||||
_onPress: string => Function;
|
||||
|
||||
/**
|
||||
* Returns a function that is used in the onPress callback of the items.
|
||||
*
|
||||
* @param {string} url - The URL of the item to navigate to.
|
||||
* @private
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onPress(url) {
|
||||
return () => {
|
||||
const { disabled, onPress } = this.props;
|
||||
|
||||
!disabled && url && typeof onPress === 'function' && onPress(url);
|
||||
};
|
||||
}
|
||||
|
||||
_onRefresh: () => void;
|
||||
|
||||
/**
|
||||
* Invokes the onRefresh callback if present.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRefresh() {
|
||||
const { onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
onRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
_renderItem: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a single item in the list.
|
||||
*
|
||||
* @param {Object} listItem - The item to render.
|
||||
* @private
|
||||
* @returns {Component}
|
||||
*/
|
||||
_renderItem(listItem) {
|
||||
const { item: { colorBase, lines, title, url } } = listItem;
|
||||
|
||||
// XXX The value of title cannot be undefined; otherwise, react-native
|
||||
// will throw a TypeError: Cannot read property of undefined. While it's
|
||||
// difficult to get an undefined title and very likely requires the
|
||||
// execution of incorrect source code, it is undesirable to break the
|
||||
// whole app because of an undefined string.
|
||||
if (typeof title === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress = { this._onPress(url) }
|
||||
underlayColor = { UNDERLAY_COLOR }>
|
||||
<View style = { styles.listItem }>
|
||||
<View style = { styles.avatarContainer } >
|
||||
<View
|
||||
style = { [
|
||||
styles.avatar,
|
||||
this._getAvatarColor(colorBase)
|
||||
] } >
|
||||
<Text style = { styles.avatarContent }>
|
||||
{ title.substr(0, 1).toUpperCase() }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style = { styles.listItemDetails }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { [
|
||||
styles.listItemText,
|
||||
styles.listItemTitle
|
||||
] }>
|
||||
{ title }
|
||||
</Text>
|
||||
{ this._renderItemLines(lines) }
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
|
||||
_renderItemLine: (string, number) => React$Node;
|
||||
|
||||
/**
|
||||
* Renders a single line from the additional lines.
|
||||
*
|
||||
* @param {string} line - The line text.
|
||||
* @param {number} index - The index of the line.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderItemLine(line, index) {
|
||||
if (!line) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
key = { index }
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.listItemText }>
|
||||
{ line }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
_renderItemLines: Array<string> => Array<React$Node>;
|
||||
|
||||
/**
|
||||
* Renders the additional item lines, if any.
|
||||
*
|
||||
* @param {Array<string>} lines - The lines to render.
|
||||
* @private
|
||||
* @returns {Array<React$Node>}
|
||||
*/
|
||||
_renderItemLines(lines) {
|
||||
return lines && lines.length ? lines.map(this._renderItemLine) : null;
|
||||
}
|
||||
|
||||
_renderListEmptyComponent: () => Object;
|
||||
|
||||
/**
|
||||
* Renders a component to display when the list is empty.
|
||||
*
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderListEmptyComponent() {
|
||||
const { t, onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
return (
|
||||
<View style = { styles.pullToRefresh }>
|
||||
<Text style = { styles.pullToRefreshText }>
|
||||
{ t('sectionList.pullToRefresh') }
|
||||
</Text>
|
||||
<Icon
|
||||
name = 'menu-down'
|
||||
style = { styles.pullToRefreshIcon } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_renderSection: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a section title.
|
||||
*
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderSection(section) {
|
||||
return (
|
||||
<View style = { styles.listSection }>
|
||||
<Text style = { styles.listSectionText }>
|
||||
{ section.section.title }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(NavigateSectionList);
|
||||
@@ -0,0 +1,48 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { Icon } from '../../../font-icons';
|
||||
import { translate } from '../../../i18n';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React Native {@link Component} that is to be displayed when the
|
||||
* list is empty
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class NavigateSectionListEmptyComponent extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.pullToRefresh }>
|
||||
<Text style = { styles.pullToRefreshText }>
|
||||
{ t('sectionList.pullToRefresh') }
|
||||
</Text>
|
||||
<Icon
|
||||
name = 'menu-down'
|
||||
style = { styles.pullToRefreshIcon } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(NavigateSectionListEmptyComponent);
|
||||
@@ -0,0 +1,141 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Container from './Container';
|
||||
import Text from './Text';
|
||||
import styles, { UNDERLAY_COLOR } from './styles';
|
||||
import type { Item } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* item containing data to be rendered
|
||||
*/
|
||||
item: Item,
|
||||
|
||||
/**
|
||||
* Function to be invoked when an Item is pressed. The Item's URL is passed.
|
||||
*/
|
||||
onPress: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React/Native {@link Component} that renders the Navigate Section
|
||||
* List Item
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class NavigateSectionListItem extends Component<Props> {
|
||||
/**
|
||||
* Constructor of the NavigateSectionList component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this._getAvatarColor = this._getAvatarColor.bind(this);
|
||||
this._renderItemLine = this._renderItemLine.bind(this);
|
||||
this._renderItemLines = this._renderItemLines.bind(this);
|
||||
}
|
||||
|
||||
_getAvatarColor: string => Object;
|
||||
|
||||
/**
|
||||
* Returns a style (color) based on the string that determines the color of
|
||||
* the avatar.
|
||||
*
|
||||
* @param {string} colorBase - The string that is the base of the color.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getAvatarColor(colorBase) {
|
||||
if (!colorBase) {
|
||||
return null;
|
||||
}
|
||||
let nameHash = 0;
|
||||
|
||||
for (let i = 0; i < colorBase.length; i++) {
|
||||
nameHash += colorBase.codePointAt(i);
|
||||
}
|
||||
|
||||
return styles[`avatarColor${(nameHash % 5) + 1}`];
|
||||
}
|
||||
|
||||
_renderItemLine: (string, number) => React$Node;
|
||||
|
||||
/**
|
||||
* Renders a single line from the additional lines.
|
||||
*
|
||||
* @param {string} line - The line text.
|
||||
* @param {number} index - The index of the line.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderItemLine(line, index) {
|
||||
if (!line) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
key = { index }
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.listItemText }>
|
||||
{line}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
_renderItemLines: Array<string> => Array<React$Node>;
|
||||
|
||||
/**
|
||||
* Renders the additional item lines, if any.
|
||||
*
|
||||
* @param {Array<string>} lines - The lines to render.
|
||||
* @private
|
||||
* @returns {Array<React$Node>}
|
||||
*/
|
||||
_renderItemLines(lines) {
|
||||
return lines && lines.length ? lines.map(this._renderItemLine) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { colorBase, lines, title } = this.props.item;
|
||||
const avatarStyles = {
|
||||
...styles.avatar,
|
||||
...this._getAvatarColor(colorBase)
|
||||
};
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { this.props.onPress }
|
||||
style = { styles.listItem }
|
||||
underlayColor = { UNDERLAY_COLOR }>
|
||||
<Container style = { styles.avatarContainer }>
|
||||
<Container style = { avatarStyles }>
|
||||
<Text style = { styles.avatarContent }>
|
||||
{title.substr(0, 1).toUpperCase()}
|
||||
</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
<Container style = { styles.listItemDetails }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = {{
|
||||
...styles.listItemText,
|
||||
...styles.listItemTitle
|
||||
}}>
|
||||
{title}
|
||||
</Text>
|
||||
{this._renderItemLines(lines)}
|
||||
</Container>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Container from './Container';
|
||||
import styles from './styles';
|
||||
import Text from './Text';
|
||||
import type { SetionListSection } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* A section containing the data to be rendered
|
||||
*/
|
||||
section: SetionListSection
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React/Native {@link Component} that renders the section header
|
||||
* of the list
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class NavigateSectionListSectionHeader extends Component<Props> {
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { section } = this.props.section;
|
||||
|
||||
return (
|
||||
<Container style = { styles.listSection }>
|
||||
<Text style = { styles.listSectionText }>
|
||||
{ section.title }
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
91
react/features/base/react/components/native/SectionList.js
Normal file
91
react/features/base/react/components/native/SectionList.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
SafeAreaView,
|
||||
SectionList as ReactNativeSectionList
|
||||
} from 'react-native';
|
||||
|
||||
import styles from './styles';
|
||||
import type { Section } from '../../Types';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SectionList}
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Rendered when the list is empty. Can be a React Component Class, a render
|
||||
* function, or a rendered element.
|
||||
*/
|
||||
ListEmptyComponent: Object,
|
||||
|
||||
/**
|
||||
*
|
||||
* Used to extract a unique key for a given item at the specified index.
|
||||
* Key is used for caching and as the react key to track item re-ordering.
|
||||
*/
|
||||
keyExtractor: Function,
|
||||
|
||||
/**
|
||||
*
|
||||
* Functions that defines what happens when the list is pulled for refresh
|
||||
*/
|
||||
onRefresh: Function,
|
||||
|
||||
/**
|
||||
*
|
||||
* A boolean that is set true while waiting for new data from a refresh.
|
||||
*/
|
||||
refreshing: ?boolean,
|
||||
|
||||
/**
|
||||
*
|
||||
* Default renderer for every item in every section.
|
||||
*/
|
||||
renderItem: Function,
|
||||
|
||||
/**
|
||||
*
|
||||
* A component rendered at the top of each section. These stick to the top
|
||||
* of the ScrollView by default on iOS.
|
||||
*/
|
||||
renderSectionHeader: Object,
|
||||
|
||||
/**
|
||||
* An array of sections
|
||||
*/
|
||||
sections: Array<Section>
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React Native {@link Component} that wraps the React Native
|
||||
* SectionList component in a SafeAreaView so that it renders the sectionlist
|
||||
* within the safe area of the device
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class SectionList extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<SafeAreaView
|
||||
style = { styles.container } >
|
||||
<ReactNativeSectionList
|
||||
ListEmptyComponent = { this.props.ListEmptyComponent }
|
||||
keyExtractor = { this.props.keyExtractor }
|
||||
onRefresh = { this.props.onRefresh }
|
||||
refreshing = { this.props.refreshing }
|
||||
renderItem = { this.props.renderItem }
|
||||
renderSectionHeader = { this.props.renderSectionHeader }
|
||||
sections = { this.props.sections }
|
||||
style = { styles.list } />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
export { default as Container } from './Container';
|
||||
export { default as Header } from './Header';
|
||||
export { default as NavigateSectionList } from './NavigateSectionList';
|
||||
export { default as Link } from './Link';
|
||||
export { default as LoadingIndicator } from './LoadingIndicator';
|
||||
export { default as NavigateSectionListEmptyComponent } from
|
||||
'./NavigateSectionListEmptyComponent';
|
||||
export { default as NavigateSectionListItem }
|
||||
from './NavigateSectionListItem';
|
||||
export { default as NavigateSectionListSectionHeader }
|
||||
from './NavigateSectionListSectionHeader';
|
||||
export { default as PagedList } from './PagedList';
|
||||
export { default as Pressable } from './Pressable';
|
||||
export { default as SideBar } from './SideBar';
|
||||
export { default as Text } from './Text';
|
||||
export { default as SectionList } from './SectionList';
|
||||
export { default as TintedView } from './TintedView';
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Container from './Container';
|
||||
import Text from './Text';
|
||||
import type { Item } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Function to be invoked when an item is pressed. The item's URL is passed.
|
||||
*/
|
||||
onPress: Function,
|
||||
|
||||
/**
|
||||
* A item containing data to be rendered
|
||||
*/
|
||||
item: Item
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React/Web {@link Component} for displaying an item in a
|
||||
* NavigateSectionList
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class NavigateSectionListItem extends Component<Props> {
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { lines, title } = this.props.item;
|
||||
const { onPress } = this.props;
|
||||
|
||||
/**
|
||||
* Initiliazes the date and duration of the conference to the an empty
|
||||
* string in case for some reason there is an error where the item data
|
||||
* lines doesn't contain one or both of those values (even though this
|
||||
* unlikely the app shouldn't break because of it)
|
||||
* @type {string}
|
||||
*/
|
||||
let date = '';
|
||||
let duration = '';
|
||||
|
||||
if (lines[0]) {
|
||||
date = lines[0];
|
||||
}
|
||||
if (lines[1]) {
|
||||
duration = lines[1];
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
className = 'navigate-section-list-tile'
|
||||
onClick = { onPress }>
|
||||
<Text
|
||||
className = 'navigate-section-tile-title'>
|
||||
{ title }
|
||||
</Text>
|
||||
<Text
|
||||
className = 'navigate-section-tile-body'>
|
||||
{ date }
|
||||
</Text>
|
||||
<Text
|
||||
className = 'navigate-section-tile-body'>
|
||||
{ duration }
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Text from './Text';
|
||||
import type { Section } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* A section containing the data to be rendered
|
||||
*/
|
||||
section: Section
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React/Web {@link Component} that renders the section header of
|
||||
* the list
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class NavigateSectionListSectionHeader extends Component<Props> {
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Text className = 'navigate-section-section-header'>
|
||||
{ this.props.section.title }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
92
react/features/base/react/components/web/SectionList.js
Normal file
92
react/features/base/react/components/web/SectionList.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Container from './Container';
|
||||
import type { Section } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Used to extract a unique key for a given item at the specified index.
|
||||
* Key is used for caching and as the react key to track item re-ordering.
|
||||
*/
|
||||
keyExtractor: Function,
|
||||
|
||||
/**
|
||||
* Returns a React component that renders each Item in the list
|
||||
*/
|
||||
renderItem: Function,
|
||||
|
||||
/**
|
||||
* Returns a React component that renders the header for every section
|
||||
*/
|
||||
renderSectionHeader: Function,
|
||||
|
||||
/**
|
||||
* An array of sections
|
||||
*/
|
||||
sections: Array<Section>,
|
||||
|
||||
/**
|
||||
* defines what happens when an item in the section list is clicked
|
||||
*/
|
||||
onItemClick: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React/Web {@link Component} for displaying a list with
|
||||
* sections similar to React Native's {@code SectionList} in order to
|
||||
* faciliate cross-platform source code.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class SectionList extends Component<Props> {
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
renderSectionHeader,
|
||||
renderItem,
|
||||
sections,
|
||||
keyExtractor
|
||||
} = this.props;
|
||||
|
||||
/**
|
||||
* If there are no recent items we dont want to display anything
|
||||
*/
|
||||
if (sections) {
|
||||
return (
|
||||
/* eslint-disable no-extra-parens */
|
||||
<Container
|
||||
className = 'navigate-section-list'>
|
||||
{
|
||||
sections.map((section, sectionIndex) => (
|
||||
<Container
|
||||
key = { sectionIndex }>
|
||||
{ renderSectionHeader(section) }
|
||||
{ section.data
|
||||
.map((item, listIndex) => {
|
||||
const listItem = {
|
||||
item
|
||||
};
|
||||
|
||||
return renderItem(listItem,
|
||||
keyExtractor(section,
|
||||
listIndex));
|
||||
}) }
|
||||
</Container>
|
||||
)
|
||||
)
|
||||
}
|
||||
</Container>
|
||||
/* eslint-enable no-extra-parens */
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
export { default as Container } from './Container';
|
||||
export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete';
|
||||
export { default as NavigateSectionListEmptyComponent } from
|
||||
'./NavigateSectionListEmptyComponent';
|
||||
export { default as NavigateSectionListItem } from
|
||||
'./NavigateSectionListItem';
|
||||
export { default as NavigateSectionListSectionHeader }
|
||||
from './NavigateSectionListSectionHeader';
|
||||
export { default as SectionList } from './SectionList';
|
||||
export { default as Text } from './Text';
|
||||
export { default as Watermarks } from './Watermarks';
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './components';
|
||||
export { default as Platform } from './Platform';
|
||||
export * from './Types';
|
||||
|
||||
14
react/features/base/session/actionTypes.js
Normal file
14
react/features/base/session/actionTypes.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* {
|
||||
* type: SET_SESSION,
|
||||
* session: {
|
||||
* url: {string},
|
||||
* state: {string},
|
||||
* ...data
|
||||
* }
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_SESSION = Symbol('SET_SESSION');
|
||||
16
react/features/base/session/actions.js
Normal file
16
react/features/base/session/actions.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { SET_SESSION } from './actionTypes';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {string} session - FIXME.
|
||||
* @returns {{
|
||||
* type: SET_SESSION
|
||||
* }}
|
||||
*/
|
||||
export function setSession(session) {
|
||||
return {
|
||||
type: SET_SESSION,
|
||||
session
|
||||
};
|
||||
}
|
||||
12
react/features/base/session/constants.js
Normal file
12
react/features/base/session/constants.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
export const SESSION_CONFIGURED = Symbol('SESSION_CONFIGURED');
|
||||
|
||||
export const SESSION_ENDED = Symbol('SESSION_ENDED');
|
||||
|
||||
export const SESSION_FAILED = Symbol('SESSION_FAILED');
|
||||
|
||||
export const SESSION_STARTED = Symbol('SESSION_STARTED');
|
||||
|
||||
export const SESSION_WILL_END = Symbol('SESSION_WILL_END');
|
||||
|
||||
export const SESSION_WILL_START = Symbol('SESSION_WILL_START');
|
||||
36
react/features/base/session/functions.js
Normal file
36
react/features/base/session/functions.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// @flow
|
||||
|
||||
import { toState } from '../redux';
|
||||
import { toURLString } from '../util';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Function|Object} stateful - FIXME.
|
||||
* @param {string} url - FIXME.
|
||||
* @returns {*}
|
||||
*/
|
||||
export function getSession(stateful: Function | Object, url: string): ?Object {
|
||||
const state = toState(stateful);
|
||||
|
||||
const session = state['features/base/session'].get(url);
|
||||
|
||||
if (!session) {
|
||||
console.info(`SESSION NOT FOUND FOR URL: ${url}`);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Function | Object} stateful - FIXME.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getCurrentSession(stateful: Function | Object): ?Object {
|
||||
const state = toState(stateful);
|
||||
const { locationURL } = state['features/base/config'];
|
||||
|
||||
return getSession(state, toURLString(locationURL));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user