Compare commits

..

5 Commits

Author SHA1 Message Date
paweldomas
4d60a4e3a6 ref(base/session): createSession on APP_WILL_NAVIGATE 2019-05-08 14:48:53 -05:00
paweldomas
160380dac0 ref(base/session): create tracks and connect on SET_ROOM 2019-05-08 14:46:18 -05:00
paweldomas
9dbba7b119 ref(base/session): move endAllSessions to APP_WILL_NAVIGATE 2019-05-08 14:39:18 -05:00
paweldomas
7101f90b6e feat: add base/session 2019-05-06 16:06:56 -05:00
paweldomas
621ee7b447 ref(base/connection/actions.native): JitsiConnection.connect returns void
Do not return anything from JitsiConnection.connect, because it's not
a promise and returns void. Doing so is confusing to the reader.
2019-05-06 16:06:56 -05:00
133 changed files with 1782 additions and 3969 deletions

View File

@@ -2,6 +2,5 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:navigationBarColor">#1081B2</item>
</style>
</resources>

View File

@@ -10,7 +10,6 @@ MVN_HTTP=0
DEFAULT_SDK_VERSION=$(grep sdkVersion ${THIS_DIR}/../gradle.properties | cut -d"=" -f2)
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
RN_VERSION=$(jq -r '.dependencies."react-native"' ${THIS_DIR}/../../package.json)
DO_GIT_TAG=${GIT_TAG:-0}
if [[ $THE_MVN_REPO == http* ]]; then
MVN_HTTP=1
@@ -65,15 +64,17 @@ pushd ${THIS_DIR}/../
./gradlew clean assembleRelease publish
popd
if [[ $DO_GIT_TAG == 1 ]]; then
if [[ $MVN_HTTP == 0 ]]; then
# The artifacts are now on the Maven repo, commit them
pushd ${MVN_REPO_PATH}
git add -A .
git commit -m "Jitsi Meet SDK + dependencies: ${SDK_VERSION}"
if [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" == "true" ]]; then
git add -A .
git commit -m "Jitsi Meet SDK + dependencies"
fi
popd
# Tag the release
git tag android-sdk-${SDK_VERSION}
git tag -a android-sdk-${SDK_VERSION}
fi
# Done!

View File

@@ -53,7 +53,6 @@ dependencies {
implementation project(':react-native-background-timer')
implementation project(':react-native-calendar-events')
implementation project(':react-native-community-async-storage')
implementation(project(':react-native-fast-image')) {
exclude group: 'com.android.support'
}
@@ -63,7 +62,6 @@ dependencies {
implementation project(':react-native-sound')
implementation project(':react-native-vector-icons')
implementation project(':react-native-webrtc')
implementation project(':react-native-webview')
testImplementation 'junit:junit:4.12'
}
@@ -207,7 +205,8 @@ publishing {
def groupId = it.moduleGroup
def artifactId = it.moduleName
if (artifactId.startsWith('react-native-') && groupId.equals('jitsi-meet')) {
if (artifactId.startsWith('react-native-')
&& groupId.equals('jitsi-meet')) {
groupId = rootProject.ext.moduleGroupId
}

View File

@@ -40,15 +40,6 @@ public class JitsiMeetActivityDelegate {
private static PermissionListener permissionListener;
private static Callback permissionsCallback;
/**
* Tells whether or not the permissions request is currently in progress.
*
* @return {@code true} if the permssions are being requested or {@code false} otherwise.
*/
static boolean arePermissionsBeingRequested() {
return permissionListener != null;
}
/**
* {@link Activity} lifecycle method which should be called from
* {@code Activity#onActivityResult} so we are notified about results of external intents

View File

@@ -123,7 +123,6 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener> {
PictureInPictureModule.class);
if (pipModule != null
&& PictureInPictureModule.isPictureInPictureSupported()
&& !JitsiMeetActivityDelegate.arePermissionsBeingRequested()
&& this.url != null) {
try {
pipModule.enterPictureInPicture();

View File

@@ -147,8 +147,6 @@ class ReactInstanceManagerHolder {
new com.oblador.vectoricons.VectorIconsPackage(),
new com.ocetnik.timer.BackgroundTimerPackage(),
new com.oney.WebRTCModule.WebRTCModulePackage(),
new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
new com.reactnativecommunity.webview.RNCWebViewPackage(),
new com.rnimmersive.RNImmersivePackage(),
new com.zmxv.RNSound.RNSoundPackage(),
new ReactPackageAdapter() {

View File

@@ -3,10 +3,6 @@ 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-calendar-events'
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')
include ':react-native-community-async-storage'
project(':react-native-community-async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/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-google-signin'
@@ -23,5 +19,5 @@ include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-webrtc'
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
include ':react-native-calendar-events'
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')

View File

@@ -718,21 +718,13 @@ export default {
this.roomName = options.roomName;
return (
// Initialize the device list first. This way, when creating tracks
// based on preferred devices, loose label matching can be done in
// cases where the exact ID match is no longer available, such as
// when the camera device has switched USB ports.
this._initDeviceList()
.catch(error => logger.warn(
'initial device list initialization failed', error))
.then(() => this.createInitialLocalTracksAndConnect(
this.createInitialLocalTracksAndConnect(
options.roomName, {
startAudioOnly: config.startAudioOnly,
startScreenSharing: config.startScreenSharing,
startWithAudioMuted: config.startWithAudioMuted,
startWithVideoMuted: config.startWithVideoMuted
}))
})
.then(([ tracks, con ]) => {
tracks.forEach(track => {
if ((track.isAudioTrack() && this.isLocalAudioMuted())
@@ -777,10 +769,7 @@ export default {
this.setVideoMuteStatus(true);
}
// Initialize device list a second time to ensure device labels
// get populated in case of an initial gUM acceptance; otherwise
// they may remain as empty strings.
this._initDeviceList(true);
this._initDeviceList();
if (config.iAmRecorder) {
this.recorder = new Recorder();
@@ -2288,23 +2277,20 @@ export default {
},
/**
* Updates the list of current devices.
* @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
* Inits list of current devices and event listener for device change.
* @private
* @returns {Promise}
*/
_initDeviceList(setDeviceListChangeHandler = false) {
_initDeviceList() {
const { mediaDevices } = JitsiMeetJS;
if (mediaDevices.isDeviceListAvailable()
&& mediaDevices.isDeviceChangeAvailable()) {
if (setDeviceListChangeHandler) {
this.deviceChangeListener = devices =>
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
mediaDevices.addEventListener(
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
this.deviceChangeListener);
}
this.deviceChangeListener = devices =>
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
mediaDevices.addEventListener(
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
this.deviceChangeListener);
const { dispatch } = APP.store;
@@ -2402,26 +2388,17 @@ export default {
// Let's handle unknown/non-preferred devices
const newAvailDevices
= APP.store.getState()['features/base/devices'].availableDevices;
let newAudioDevices = [];
let oldAudioDevices = [];
if (typeof newDevices.audiooutput === 'undefined') {
newAudioDevices = newAvailDevices.audioOutput;
oldAudioDevices = oldDevices.audioOutput;
APP.store.dispatch(
checkAndNotifyForNewDevice(newAvailDevices.audioOutput, oldDevices.audioOutput));
}
if (!requestedInput.audio) {
newAudioDevices = newAudioDevices.concat(newAvailDevices.audioInput);
oldAudioDevices = oldAudioDevices.concat(oldDevices.audioInput);
}
// check for audio
if (newAudioDevices.length > 0) {
APP.store.dispatch(
checkAndNotifyForNewDevice(newAudioDevices, oldAudioDevices));
checkAndNotifyForNewDevice(newAvailDevices.audioInput, oldDevices.audioInput));
}
// check for video
if (!requestedInput.video) {
APP.store.dispatch(
checkAndNotifyForNewDevice(newAvailDevices.videoInput, oldDevices.videoInput));

View File

@@ -266,13 +266,6 @@ var config = {
// Whether or not some features are checked based on token.
// enableFeaturesBasedOnToken: false,
// Enable lock room for all moderators, even when userRolesBasedOnToken is enabled and participants are guests.
// lockRoomGuestEnabled: false,
// When enabled the password used for locking a room is restricted to up to the number of digits specified
// roomPasswordNumberOfDigits: 10,
// default: roomPasswordNumberOfDigits: false,
// Message to show the users. Example: 'The service will be down for
// maintenance at 01:00 AM GMT,
// noticeMessage: '',

View File

@@ -5,7 +5,8 @@ import jitsiLocalStorage from './modules/util/JitsiLocalStorage';
import {
connectionEstablished,
connectionFailed
connectionFailed,
connectionWillConnect
} from './react/features/base/connection';
import {
isFatalJitsiConnectionError,
@@ -74,6 +75,7 @@ function checkForAttachParametersAndConnect(id, password, connection) {
function connect(id, password, roomName) {
const connectionConfig = Object.assign({}, config);
const { issuer, jwt } = APP.store.getState()['features/base/jwt'];
const { locationURL } = APP.store.getState()['features/base/connection'];
connectionConfig.bosh += `?room=${roomName}`;
@@ -83,6 +85,8 @@ function connect(id, password, roomName) {
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
connectionConfig);
APP.store.dispatch(connectionWillConnect(connection, locationURL));
return new Promise((resolve, reject) => {
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_ESTABLISHED,

View File

@@ -1,9 +1,6 @@
#sideToolbarContainer {
background-color: $newToolbarBackgroundColor;
box-sizing: border-box;
color: #FFF;
display: flex;
flex-direction: column;
/**
* Make the sidebar flush with the top of the toolbar. Take the size of
* the toolbar and subtract from 100%.
@@ -24,6 +21,20 @@
&.slideInExt {
left: 0;
}
.sideToolbarContainer__inner {
box-sizing: border-box;
color: #FFF;
display: flex;
flex-direction: column;
height: 100%;
width: $sidebarWidth;
}
}
#chat_container * {
-webkit-user-select: text;
user-select: text;
}
#chatconversation {
@@ -31,8 +42,9 @@
flex: 1;
font-size: 10pt;
line-height: 20px;
margin-top: $desktopAppDragBarHeight + 5px;
overflow: auto;
padding: 16px;
padding: 5px;
text-align: left;
width: $sidebarWidth;
word-wrap: break-word;
@@ -80,41 +92,26 @@
}
}
.chat-header {
background-color: $chatHeaderBackgroundColor;
height: 70px;
position: relative;
width: 100%;
.chat-close {
background: gray;
border: 3px solid rgba(255, 255, 255, 0.1);
border-radius: 100%;
color: white;
cursor:pointer;
height: 10px;
line-height: 10px;
padding: 4px;
position: absolute;
right: 5px;
text-align: center;
top: $desktopAppDragBarHeight;
width: 10px;
z-index: 1;
.chat-close {
align-items: center;
bottom: 8px;
color: white;
cursor: pointer;
display: flex;
font-size: 18px;
height: 40px;
justify-content: center;
line-height: 15px;
padding: 4px;
position: absolute;
right: 5px;
width: 40px;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
}
}
#chat-input {
border-top: 1px solid $chatInputSeparatorColor;
background-color: $newToolbarBackgroundColor;
display: flex;
* {
background-color: transparent;
}
}
.remoteuser {
@@ -126,13 +123,16 @@
}
#usermsg {
background-color: $newToolbarBackgroundColor;
border: 0px none;
border-radius:0;
box-shadow: none;
color: white;
font-size: 15px;
font-size: 10pt;
line-height: 30px;
padding: 5px;
max-height:150px;
min-height:35px;
overflow-y: auto;
resize: none;
width: 100%;
@@ -145,47 +145,64 @@
}
#nickname {
position: absolute;
text-align: center;
color: #9d9d9d;
font-size: 18px;
margin-top: 30px;
top: 100px;
left: 5px;
right: 5px;
width: 95%;
}
.sideToolbarContainer {
* {
-webkit-user-select: text;
user-select: text;
}
#chat_container .display-name {
float: left;
padding-left: 5px;
font-weight: bold;
white-space: nowrap;
text-overflow: ellipsis;
width: 95%;
overflow: hidden;
}
.display-name {
font-size: 13px;
font-weight: bold;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
#chat_container .timestamp {
float: right;
padding-right: 5px;
font-size: 11px;
}
.usermessage {
padding-top: 20px;
padding-left: 5px;
}
.chatArrow {
border-color:
transparent $newToolbarBackgroundColor transparent transparent;
border-style: solid;
border-width: 0 10px 10px 0;
left: -10px;
position: absolute;
}
.chatmessage {
background-color: $chatRemoteMessageBackgroundColor;
border-radius: 0px 6px 6px 6px;
box-sizing: border-box;
color: white;
background-color: $newToolbarBackgroundColor;
width: 93%;
margin-left: 9px;
margin-right: auto;
border-radius: 5px;
border-top-left-radius: 0px;
margin-top: 3px;
max-width: 100%;
color: white;
padding-bottom: 3px;
position: relative;
&.localuser {
background-color: $chatLocalMessageBackgroundColor;
border-radius: 6px 0px 6px 6px;
&.localuser .display-name {
color: #4C9AFF
}
&.error {
border-radius: 0px;
.chatArrow,
.timestamp,
.display-name {
display: none;
@@ -214,6 +231,7 @@
}
#smileysarea {
background-color: $newToolbarBackgroundColor;
display: flex;
max-height: 150px;
min-height: 35px;
@@ -228,22 +246,14 @@
.smileys-panel {
bottom: 100%;
box-sizing: border-box;
height: auto;
max-height: 0;
height: 0;
overflow: hidden;
position: absolute;
transition: height 0.3s;
width: $sidebarWidth;
/**
* CSS transitions do not apply for auto dimensions. So to produce the css
* accordion effect for showing and hiding the smiley-panel, while allowing
* for variable panel, height, use a very large max-height and animate off
* of that.
*/
transition: max-height 0.3s;
&.show-smileys {
max-height: 500%;
height: 146px;
}
#smileysContainer {
@@ -277,49 +287,3 @@
#usermsg::-webkit-scrollbar-track-piece {
background: #3a3a3a;
}
.chat-message-group {
display: flex;
flex-direction: column;
&.local {
align-items: flex-end;
.chatmessage {
background-color: $chatLocalMessageBackgroundColor;
border-radius: 6px 0px 6px 6px;
}
.display-name {
display: none;
}
.timestamp {
text-align: right;
}
}
&.error {
.chatmessage {
border-radius: 0px;
color: red;
}
.display-name {
display: none;
}
}
.chatmessage-wrapper {
max-width: 100%;
}
.chatmessage {
background-color: $chatRemoteMessageBackgroundColor;
border-radius: 0px 6px 6px 6px;
display: inline-block;
margin-top: 3px;
color: white;
padding: 8px;
}
}

View File

@@ -83,19 +83,11 @@ $modalMockAKInputBorder: 1px solid #f4f5f7;
$modalTextColor: #333;
/**
* Chat
*/
$chatHeaderBackgroundColor: rgba(42, 58, 75, 0.9);
$chatInputSeparatorColor: #A4B8D1;
$chatLocalMessageBackgroundColor: rgba(26, 108, 180, 1);
$chatRemoteMessageBackgroundColor: rgba(240, 243, 247, 0.15);
$sidebarWidth: 375px;
/**
* Misc.
*/
$borderRadius: 4px;
$defaultWatermarkLink: '../images/watermark.png';
$sidebarWidth: 220px;
$popoverMenuPadding: 13px;
$happySoftwareBackground: transparent;
$desktopAppDragBarHeight: 25px;

View File

@@ -162,10 +162,4 @@ body.welcome-page {
font-size: 32px;
}
}
.welcome-watermark {
position: absolute;
width: 100%;
height: 100%;
}
}

View File

@@ -185,7 +185,6 @@
font-size: 12px;
max-height: 100%;
overflow: auto;
padding: 15pt;
position: absolute;
transform: translateY(-50%);
top: 50%;

View File

@@ -222,11 +222,6 @@ api.executeCommand('toggleChat');
api.executeCommand('toggleShareScreen');
```
* **toggleTileView** - Enter / exit tile view layout mode. No arguments are required.
```javascript
api.executeCommand('toggleTileView');
```
* **hangup** - Hangups the call. No arguments are required.
```javascript
api.executeCommand('hangup');
@@ -301,13 +296,6 @@ changes. The listener will receive an object with the following structure:
}
```
* **tileViewChanged** - event notifications about tile view layout mode being entered or exited. The listener will receive object with the following structure:
```javascript
{
enabled: boolean, // whether tile view is not displayed or not
}
```
* **incomingMessage** - Event notifications about incoming
messages. The listener will receive an object with the following structure:
```javascript

View File

@@ -5,7 +5,7 @@ signed Android build for that, that can be a debug self-signed build too, just
retrieve the signing hash. The key hash of an already signed ap can be obtained
as follows (on macOS): ```keytool -list -printcert -jarfile the-app.apk```
- Place the generated ```google-services.json``` file in ```android/app```
for Android and the ```GoogleService-Info.plist``` into ```ios/app``` for
for Android and the ```GoogleService-Info.plist``` into ```ios/app/src``` for
iOS (you can stop at that step, no need for the driver and the code changes they
suggest in the wizard).
- You may want to exclude these files in YOUR GIT config (do not exclude them in

View File

@@ -167,13 +167,7 @@ var interfaceConfig = {
*
* @type {boolean}
*/
RECENT_LIST_ENABLED: true,
/**
* A UX mode where the last screen share participant is automatically
* pinned. Note: this mode is experimental and subject to breakage.
*/
AUTO_PIN_LATEST_SCREEN_SHARE: true
RECENT_LIST_ENABLED: true
/**
* How many columns the tile view can expand to. The respected range is
@@ -201,6 +195,12 @@ var interfaceConfig = {
*/
// ANDROID_APP_PACKAGE: 'org.jitsi.meet',
/**
* A UX mode where the last screen share participant is automatically
* pinned. Note: this mode is experimental and subject to breakage.
*/
// AUTO_PIN_LATEST_SCREEN_SHARE: false,
/**
* Override the behavior of some notifications to remain displayed until
* explicitly dismissed through a user action. The value is how long, in

View File

@@ -14,9 +14,6 @@ end
target 'JitsiMeet' do
project 'sdk/sdk.xcodeproj'
# React Native and its dependencies
#
pod 'React', :path => '../node_modules/react-native', :subspecs => [
'Core',
'CxxBridge',
@@ -30,31 +27,33 @@ target 'JitsiMeet' do
'RCTWebSocket',
]
pod 'yoga', :path => '../node_modules/react-native/ReactCommon/yoga'
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
# React Native plugins
#
pod 'react-native-background-timer', :path => '../node_modules/react-native-background-timer'
pod 'react-native-calendar-events', :path => '../node_modules/react-native-calendar-events'
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-webview', :path => '../node_modules/react-native-webview'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'BVLinearGradient', :path => '../node_modules/react-native-linear-gradient'
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-community/async-storage'
pod 'RNGoogleSignin', :path => '../node_modules/react-native-google-signin'
pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'RNWatch', :path => '../node_modules/react-native-watch-connectivity'
# Native pod dependencies
#
pod 'DoubleConversion',
:podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog',
:podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly',
:podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
pod 'Amplitude-iOS', '~> 4.0.4'
pod 'ObjectiveDropboxOfficial', '~> 3.9.4'
pod 'react-native-background-timer',
:path => '../node_modules/react-native-background-timer'
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 'BVLinearGradient',
:path => '../node_modules/react-native-linear-gradient'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'RNGoogleSignin',
:path => '../node_modules/react-native-google-signin'
pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'RNWatch', :path => '../node_modules/react-native-watch-connectivity'
pod 'react-native-calendar-events',
:path => '../node_modules/react-native-calendar-events'
end
post_install do |installer|

View File

@@ -84,8 +84,8 @@ PODS:
- nanopb/decode (0.3.901)
- nanopb/encode (0.3.901)
- ObjectiveDropboxOfficial (3.9.4)
- React (0.59.8):
- React/Core (= 0.59.8)
- React (0.59.5):
- React/Core (= 0.59.5)
- react-native-background-timer (2.1.1):
- React
- react-native-calendar-events (1.6.4):
@@ -99,57 +99,53 @@ PODS:
- React
- react-native-webrtc (1.69.1):
- React
- react-native-webview (5.8.1):
- React
- React/Core (0.59.8):
- yoga (= 0.59.8.React)
- React/CxxBridge (0.59.8):
- React/Core (0.59.5):
- yoga (= 0.59.5.React)
- React/CxxBridge (0.59.5):
- Folly (= 2018.10.22.00)
- React/Core
- React/cxxreact
- React/jsiexecutor
- React/cxxreact (0.59.8):
- React/cxxreact (0.59.5):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React/jsinspector
- React/DevSupport (0.59.8):
- React/DevSupport (0.59.5):
- React/Core
- React/RCTWebSocket
- React/fishhook (0.59.8)
- React/jsi (0.59.8):
- React/fishhook (0.59.5)
- React/jsi (0.59.5):
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React/jsiexecutor (0.59.8):
- React/jsiexecutor (0.59.5):
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React/cxxreact
- React/jsi
- React/jsinspector (0.59.8)
- React/RCTActionSheet (0.59.8):
- React/jsinspector (0.59.5)
- React/RCTActionSheet (0.59.5):
- React/Core
- React/RCTAnimation (0.59.8):
- React/RCTAnimation (0.59.5):
- React/Core
- React/RCTBlob (0.59.8):
- React/RCTBlob (0.59.5):
- React/Core
- React/RCTImage (0.59.8):
- React/RCTImage (0.59.5):
- React/Core
- React/RCTNetwork
- React/RCTLinkingIOS (0.59.8):
- React/RCTLinkingIOS (0.59.5):
- React/Core
- React/RCTNetwork (0.59.8):
- React/RCTNetwork (0.59.5):
- React/Core
- React/RCTText (0.59.8):
- React/RCTText (0.59.5):
- React/Core
- React/RCTWebSocket (0.59.8):
- React/RCTWebSocket (0.59.5):
- React/Core
- React/fishhook
- React/RCTBlob
- RNCAsyncStorage (1.3.4):
- React
- RNGoogleSignin (1.0.2):
- GoogleSignIn
- React
@@ -166,7 +162,7 @@ PODS:
- SDWebImage/GIF (4.4.6):
- FLAnimatedImage (~> 1.0)
- SDWebImage/Core
- yoga (0.59.8.React)
- yoga (0.59.5.React)
DEPENDENCIES:
- Amplitude-iOS (~> 4.0.4)
@@ -184,7 +180,6 @@ DEPENDENCIES:
- react-native-fast-image (from `../node_modules/react-native-fast-image`)
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React/Core (from `../node_modules/react-native`)
- React/CxxBridge (from `../node_modules/react-native`)
- React/DevSupport (from `../node_modules/react-native`)
@@ -195,7 +190,6 @@ DEPENDENCIES:
- React/RCTNetwork (from `../node_modules/react-native`)
- React/RCTText (from `../node_modules/react-native`)
- React/RCTWebSocket (from `../node_modules/react-native`)
- "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)"
- RNGoogleSignin (from `../node_modules/react-native-google-signin`)
- RNSound (from `../node_modules/react-native-sound`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
@@ -245,10 +239,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-keep-awake"
react-native-webrtc:
:path: "../node_modules/react-native-webrtc"
react-native-webview:
:path: "../node_modules/react-native-webview"
RNCAsyncStorage:
:path: "../node_modules/@react-native-community/async-storage"
RNGoogleSignin:
:path: "../node_modules/react-native-google-signin"
RNSound:
@@ -283,21 +273,19 @@ SPEC CHECKSUMS:
GTMSessionFetcher: 32aeca0aa144acea523e1c8e053089dec2cb98ca
nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48
ObjectiveDropboxOfficial: a5afefc83f6467c42c45f2253f583f2ad1ffc701
React: 76e6aa2b87d05eb6cccb6926d72685c9a07df152
React: 90adac468c7b72bf1fa6c64bf230650f851a8388
react-native-background-timer: 0d34748e53a972507c66963490c775321a88f6f2
react-native-calendar-events: ee9573e355711ac679e071be70789542431f4ce3
react-native-fast-image: 47487b71169aea34868e7b38bf870b6b3f2157c5
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-webrtc: 90a847d19deb2d7323fef8cc89ca12b8995fbc90
react-native-webview: a95842e3f351a6d2c8bc8bcc9eab689c7e7e5ad4
RNCAsyncStorage: 8e31405a9f12fbf42c2bb330e4560bfd79c18323
RNGoogleSignin: 361174d9a3090d295b06257162b560d8efc8a6ed
RNSound: e157320f503bdd4f4ee6d8542e948d54f90c3c3a
RNVectorIcons: d819334932bcda3332deb3d2c8ea4d069e0b98f9
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
SDWebImage: 3f3f0c02f09798048c47a5ed0a13f17b063572d8
yoga: 92b2102c3d373d1a790db4ab761d2b0ffc634f64
yoga: 2e571f113e8cbeb0eb752aeebc86c1bfe7a8200c
PODFILE CHECKSUM: b55338cc43312051ed83f8d9c6aadbd8c9402e6a
PODFILE CHECKSUM: 9e6bc935ea7d2974604572cc68938281a88cf35c
COCOAPODS: 1.6.1

View File

@@ -92,7 +92,7 @@ Leaves the currently active conference.
#### Universal / deep linking
In order to support Universal / deep linking, `JitsiMeet` offers 2 class
In order to support Universal / deep linking, `JitsiMeetView` offers 2 class
methods that you app's delegate should call in order for the app to follow those
links.
@@ -104,7 +104,7 @@ is useful when the host application uses other SDKs which also use linking.
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
{
return [[JitsiMeet sharedInstance] application:application
return [JitsiMeetView application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
@@ -117,7 +117,7 @@ And also one of the following:
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [[JitsiMeet sharedInstance] application:app
return [JitsiMeetView application:app
openURL:url
options: options];
}

View File

@@ -42,11 +42,11 @@
- (void)_onJitsiMeetViewDelegateEvent:(NSString *)name
withData:(NSDictionary *)data {
#if DEBUG
NSLog(
@"[%s:%d] JitsiMeetViewDelegate %@ %@",
__FILE__, __LINE__, name, data);
#if DEBUG
NSAssert(
[NSThread isMainThread],
@"JitsiMeetViewDelegate %@ method invoked on a non-main thread",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -14,7 +14,6 @@
<objects>
<controller title="Meetings" id="AgC-eL-Hgc" customClass="InterfaceController" customModule="JitsiMeetCompanion" customModuleProvider="target">
<items>
<label alignment="left" textAlignment="left" numberOfLines="0" id="OQN-sx-tDt"/>
<table alignment="left" id="gpO-ql-Xsr">
<items>
<tableRow identifier="MeetingRowType" id="GGl-av-xeJ" customClass="MeetingRowController" customModule="JitsiMeetCompanion_Extension">
@@ -40,7 +39,6 @@
</table>
</items>
<connections>
<outlet property="infoLabel" destination="OQN-sx-tDt" id="Juv-tb-SNj"/>
<outlet property="table" destination="gpO-ql-Xsr" id="aVV-iZ-z3l"/>
</connections>
</controller>

View File

@@ -16,31 +16,17 @@
*/
import WatchKit
import WatchConnectivity
import Foundation
class InterfaceController: WKInterfaceController {
@IBOutlet var infoLabel: WKInterfaceLabel!
@IBOutlet var table: WKInterfaceTable!
override func didAppear(){
self.updateUI(ExtensionDelegate.currentJitsiMeetContext)
}
func updateUI(_ newContext:JitsiMeetContext) {
if (newContext.recentURLs == nil || newContext.recentURLs!.count == 0) {
infoLabel.setText("There are no recent meetings. Please use the app on the phone to start a new call.")
table.setHidden(true)
infoLabel.setHidden(false)
} else {
updateRecents(withRecents: newContext.recentURLs!, currentContext: newContext)
table.setHidden(false)
infoLabel.setHidden(true)
}
if let recentURLsArray = newContext.recentURLs {
updateRecents(withRecents: recentURLsArray, currentContext: newContext)
}
}
private func updateRecents(withRecents recents: NSArray, currentContext: JitsiMeetContext) {
@@ -82,7 +68,7 @@ class InterfaceController: WKInterfaceController {
override func contextForSegue(withIdentifier segueIdentifier: String, in table: WKInterfaceTable, rowIndex: Int) -> Any? {
let controller = table.rowController(at: rowIndex) as! MeetingRowController
let currentContext = ExtensionDelegate.currentJitsiMeetContext
// Copy the current context and add the joinConferenceURL to trigger the command when the in-call screen is displayed
let actionContext = JitsiMeetContext(jmContext: currentContext)
actionContext.joinConferenceURL = controller.roomUrl

View File

@@ -1,15 +0,0 @@
#!/bin/bash
# This script will download a bitcode build of the WebRTC framework, if needed.
if [[ ! "$CONFIGURATION" = "Debug" ]]; then
RN_WEBRTC="$SRCROOT/../../node_modules/react-native-webrtc"
if otool -arch arm64 -l $RN_WEBRTC/ios/WebRTC.framework/WebRTC | grep -q LLVM; then
echo "WebRTC framework has bitcode"
else
echo "WebRTC framework has NO bitcode"
$RN_WEBRTC/tools/downloadBitcode.sh
fi
fi

View File

@@ -5,9 +5,7 @@ set -e -u
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
PROJECT_REPO=$(realpath ${THIS_DIR}/../..)
RELEASE_REPO=$(realpath ${THIS_DIR}/../../../jitsi-meet-ios-sdk-releases)
DEFAULT_SDK_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${THIS_DIR}/../sdk/src/Info.plist)
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
DO_GIT_TAG=${GIT_TAG:-0}
SDK_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${THIS_DIR}/../sdk/src/Info.plist)
echo "Releasing Jitsi Meet SDK ${SDK_VERSION}"
@@ -26,9 +24,7 @@ popd
pushd ${PROJECT_REPO}
rm -rf ios/sdk/JitsiMeet.framework
xcodebuild -workspace ios/jitsi-meet.xcworkspace -scheme JitsiMeet -destination='generic/platform=iOS' -configuration Release archive
if [[ $DO_GIT_TAG == 1 ]]; then
git tag ios-sdk-${SDK_VERSION}
fi
git tag -a ios-sdk-${SDK_VERSION}
popd
pushd ${RELEASE_REPO}
@@ -37,16 +33,10 @@ pushd ${RELEASE_REPO}
cp -r ${PROJECT_REPO}/ios/sdk/JitsiMeet.framework Frameworks/
cp -r ${PROJECT_REPO}/node_modules/react-native-webrtc/ios/WebRTC.framework Frameworks/
# Strip bitcode
xcrun bitcode_strip -r Frameworks/JitsiMeet.framework/JitsiMeet -o Frameworks/JitsiMeet.framework/JitsiMeet
xcrun bitcode_strip -r Frameworks/WebRTC.framework/WebRTC -o Frameworks/WebRTC.framework/WebRTC
# Add all files to git
if [[ $DO_GIT_TAG == 1 ]]; then
git add -A .
git commit -m "${SDK_VERSION}"
git tag ${SDK_VERSION}
fi
git add -A .
git commit -m "${SDK_VERSION}"
git tag ${SDK_VERSION}
popd

View File

@@ -270,7 +270,6 @@
buildConfigurationList = 0BD906ED1EC0C00300C8C18E /* Build configuration list for PBXNativeTarget "JitsiMeet" */;
buildPhases = (
26796D8589142D80C8AFDA51 /* [CP] Check Pods Manifest.lock */,
DE3D81D6228B50FB00A6C149 /* Bitcode */,
0BD906E01EC0C00300C8C18E /* Sources */,
0BD906E11EC0C00300C8C18E /* Frameworks */,
0BD906E21EC0C00300C8C18E /* Headers */,
@@ -451,24 +450,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh\"\n";
showEnvVarsInLog = 0;
};
DE3D81D6228B50FB00A6C149 /* Bitcode */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Bitcode;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "../scripts/bitcode.sh\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

View File

@@ -26,7 +26,6 @@
#import <React/RCTBridge.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTUtils.h>
#import <WebRTC/WebRTC.h>
#import <JitsiMeet/JitsiMeet-Swift.h>
@@ -308,35 +307,21 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
startedConnectingAt:nil];
}
- (void) providerDidActivateAudioSessionWithSession:(AVAudioSession *)session {
// The following just help with debugging:
#ifdef DEBUG
- (void) providerDidActivateAudioSessionWithSession:(AVAudioSession *)session {
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession:]");
#endif
[[RTCAudioSession sharedInstance] audioSessionDidActivate:session];
}
- (void) providerDidDeactivateAudioSessionWithSession:(AVAudioSession *)session {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]");
#endif
[[RTCAudioSession sharedInstance] audioSessionDidDeactivate:session];
}
- (void) providerTimedOutPerformingActionWithAction:(CXAction *)action {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]");
}
#endif
}
// The bridge might already be invalidated by the time a CallKit event is processed,
// just ignore it and don't emit it.
- (void)sendEventWithName:(NSString *)name body:(id)body {
if (!self.bridge) {
return;
}
[super sendEventWithName:name body:body];
}
@end

View File

@@ -21,7 +21,6 @@ import Foundation
internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
private let listeners = NSMutableArray()
private var pendingMuteActions = Set<UUID>()
internal override init() {}
@@ -37,12 +36,6 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
listeners.remove(listener)
}
// MARK: - Add mute action
func addMuteAction(_ actionUUID: UUID) {
pendingMuteActions.insert(actionUUID)
}
// MARK: - CXProviderDelegate
func providerDidReset(_ provider: CXProvider) {
@@ -50,7 +43,6 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
let listener = $0 as! JMCallKitListener
listener.providerDidReset?()
}
pendingMuteActions.removeAll()
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
@@ -72,20 +64,9 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
let uuid = pendingMuteActions.remove(action.uuid)
// Avoid mute actions ping-pong: if the mute action was caused by
// the JS side (we requested a transaction) don't call the delegate
// method. If it was called by the provder itself (when the user presses
// the mute button in the CallKit view) then call the delegate method.
//
// NOTE: don't try to be clever and remove this. Been there, done that.
// Won't work.
if (uuid == nil) {
listeners.forEach {
let listener = $0 as! JMCallKitListener
listener.performSetMutedCall?(UUID: action.callUUID, isMuted: action.isMuted)
}
listeners.forEach {
let listener = $0 as! JMCallKitListener
listener.performSetMutedCall?(UUID: action.callUUID, isMuted: action.isMuted)
}
action.fulfill()

View File

@@ -160,14 +160,6 @@ import Foundation
completion: @escaping (Error?) -> Swift.Void) {
guard enabled else { return }
// XXX keep track of muted actions to avoid "ping-pong"ing. See
// JMCallKitEmitter for details on the CXSetMutedCallAction handling.
for action in transaction.actions {
if (action as? CXSetMutedCallAction) != nil {
emitter.addMuteAction(action.uuid)
}
}
callController.request(transaction, completion: completion)
}
@@ -195,4 +187,3 @@ import Foundation
return update
}
}

View File

@@ -98,13 +98,11 @@ curl -L -o ${CERT_DIR}/AppleWWDRCA.cer 'http://developer.apple.com/certification
curl -L -o ${CERT_DIR}/dev-cert.cer.enc ${IOS_DEV_CERT_URL}
curl -L -o ${CERT_DIR}/dev-key.p12.enc ${IOS_DEV_CERT_KEY_URL}
curl -L -o ${CERT_DIR}/dev-profile.mobileprovision.enc ${IOS_DEV_PROV_PROFILE_URL}
curl -L -o ${CERT_DIR}/dev-watch-profile.mobileprovision.enc ${IOS_DEV_WATCH_PROV_PROFILE_URL}
curl -L -o ${CERT_DIR}/id_rsa.enc ${DEPLOY_SSH_CERT_URL}
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-cert.cer.enc -d -a -out ${CERT_DIR}/dev-cert.cer
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-key.p12.enc -d -a -out ${CERT_DIR}/dev-key.p12
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-profile.mobileprovision.enc -d -a -out ${CERT_DIR}/dev-profile.mobileprovision
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-watch-profile.mobileprovision.enc -d -a -out ${CERT_DIR}/dev-watch-profile.mobileprovision
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/id_rsa.enc -d -a -out ${CERT_DIR}/id_rsa
chmod 0600 ${CERT_DIR}/id_rsa
@@ -128,13 +126,9 @@ echo "done set-key-partition-list"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "${CERT_DIR}/dev-profile.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/
cp "${CERT_DIR}/dev-watch-profile.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/
npm install
# Ever since the Apple Watch app has been added the bitcode for WebRTC needs to be downloaded in order to build successfully
./node_modules/react-native-webrtc/tools/downloadBitcode.sh
cd ios
pod update
pod install
@@ -152,12 +146,4 @@ xcodebuild -quiet -exportArchive -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchi
echo "Will try deploy the .ipa to: ${IPA_DEPLOY_LOCATION}"
ssh-add ${CERT_DIR}/id_rsa
if [ ! -z ${SCP_PROXY_HOST} ];
then
scp -o ProxyCommand="ssh -t -A -l %r ${SCP_PROXY_HOST} -o \"StrictHostKeyChecking no\" -o \"BatchMode yes\" -W %h:%p" -o StrictHostKeyChecking=no -o LogLevel=DEBUG "${IPA_EXPORT_DIR}/jitsi-meet.ipa" "${IPA_DEPLOY_LOCATION}"
else
scp -o StrictHostKeyChecking=no -o LogLevel=DEBUG "${IPA_EXPORT_DIR}/jitsi-meet.ipa" "${IPA_DEPLOY_LOCATION}"
fi
scp -i ${CERT_DIR}/id_rsa -o StrictHostKeyChecking=no -o LogLevel=DEBUG "${IPA_EXPORT_DIR}/jitsi-meet.ipa" "${IPA_DEPLOY_LOCATION}"

View File

@@ -1,19 +1,11 @@
{
"en": "Английски",
"af": "",
"az": "",
"bg": "Български",
"cs": "",
"de": "Немски",
"el": "",
"eo": "Есперанто",
"es": "Испански",
"fr": "Френски",
"hy": "Арменски",
"it": "Италиански",
"ja": "",
"ko": "",
"nb": "Норвежки букмол",
"oc": "Окситански",
"pl": "Полски",
"ptBR": "Португалски (Бразилия)",
@@ -22,6 +14,7 @@
"sl": "Словенски",
"sv": "Шведски",
"tr": "Турски",
"vi": "",
"zhCN": "Китайски (Китай)"
"zhCN": "Китайски (Китай)",
"nb": "Норвежки букмол",
"eo": "Есперанто"
}

View File

@@ -1,18 +1,11 @@
{
"en": "Englisch",
"az": "",
"bg": "Bulgarisch",
"cs": "",
"de": "Deutsch",
"el": "",
"eo": "Esperanto",
"es": "Spanisch",
"fr": "Französisch",
"hy": "Armenisch",
"it": "Italienisch",
"ja": "",
"ko": "",
"nb": "Norwegisch (Bokmal)",
"oc": "Okzitanisch",
"pl": "Polnisch",
"ptBR": "Portugiesisch (Brasilien)",
@@ -21,6 +14,7 @@
"sl": "Slowenisch",
"sv": "Schwedisch",
"tr": "Türkisch",
"vi": "",
"zhCN": "Chinesisch (China)"
"zhCN": "Chinesisch (China)",
"nb": "Norwegisch (Bokmal)",
"eo": "Esperanto"
}

View File

@@ -1,27 +0,0 @@
{
"en": "",
"af": "",
"az": "",
"bg": "",
"cs": "",
"de": "",
"el": "",
"eo": "",
"es": "",
"fr": "",
"hy": "",
"it": "",
"ja": "",
"ko": "",
"nb": "",
"oc": "",
"pl": "",
"ptBR": "",
"ru": "",
"sk": "",
"sl": "",
"sv": "",
"tr": "",
"vi": "",
"zhCN": ""
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,43 +35,31 @@
"raiseHand": "Hand erheben",
"pushToTalk": "Drücken um zu sprechen",
"toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln",
"toggleFilmstrip": "",
"toggleShortcuts": "",
"toggleFilmstrip": "Videos anzeigen oder verbergen",
"toggleShortcuts": "Hilfe-Menu anzeigen oder verdecken",
"focusLocal": "Lokales Video fokussieren",
"focusRemote": "Auf das Video eines anderen Teilnehmers fokussieren",
"toggleChat": "Chat öffnen oder schliessen",
"mute": "Stummschaltung aktivieren oder deaktivieren",
"fullScreen": "Vollbildmodus aktivieren / deaktivieren",
"videoMute": "Kamera starten oder stoppen",
"showSpeakerStats": "Statistiken für Sprecher anzeigen",
"localRecording": ""
"showSpeakerStats": "Statistiken für Sprecher anzeigen"
},
"\u0005keyboardShortcuts": {},
"welcomepage": {
"accessibilityLabel": {
"join": "",
"roomname": "Konferenzname eingeben"
},
"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",
"connectCalendarText": "",
"connectCalendarButton": "",
"enterRoomTitle": "",
"go": "Los",
"join": "Beitreten",
"privacy": "Privatsphäre",
"recentList": "",
"recentListDelete": "",
"recentListEmpty": "",
"roomname": "Konferenzname eingeben",
"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",
"title": ""
"title": "Sichere, flexible und vollständig freie Videokonferenzen"
},
"startupoverlay": {
"policyText": "",
@@ -83,41 +71,8 @@
"rejoinKeyTitle": "Erneut teilnehmen"
},
"toolbar": {
"accessibilityLabel": {
"audioOnly": "",
"audioRoute": "",
"callQuality": "Qualitätseinstellungen",
"chat": "",
"cc": "",
"document": "Geteiltes Dokument schliessen",
"feedback": "Feedback hinterlasen",
"fullScreen": "",
"hangup": "",
"invite": "Teilnehmer einladen",
"localRecording": "",
"lockRoom": "",
"moreActions": "",
"moreActionsMenu": "",
"mute": "",
"pip": "",
"profile": "Profil bearbeiten",
"raiseHand": "",
"recording": "",
"Settings": "",
"sharedvideo": "",
"shareRoom": "",
"shareYourScreen": "",
"shortcuts": "",
"speakerStats": "",
"toggleCamera": "",
"tileView": "",
"videomute": ""
},
"addPeople": "Teilnehmer zur Konferenz hinzufügen",
"audioonly": "Nur-Audio-Modus aktivieren/deaktivieren (spart Bandbreite)",
"audioOnlyOn": "Nur-Audio-Modus aktivieren/deaktivieren (spart Bandbreite)",
"audioOnlyOff": "",
"audioRoute": "",
"callQuality": "Qualitätseinstellungen",
"enterFullScreen": "Vollbildmodus",
"exitFullScreen": "Vollbildmodus verlassen",
@@ -131,8 +86,8 @@
"etherpad": "Geteiltes Dokument öffnen / schliessen",
"documentOpen": "Geteiltes Dokument öffnen",
"documentClose": "Geteiltes Dokument schliessen",
"shareRoom": "",
"sharedvideo": "YouTube-Video teilen",
"sharescreen": "Bildschirmfreigabe",
"stopSharedVideo": "YouTube Video stoppen",
"fullscreen": "Vollbildmodus aktivieren / deaktivieren",
"sip": "SIP Nummer anrufen",
@@ -141,40 +96,25 @@
"login": "Anmelden",
"logout": "Abmelden",
"sharedVideoMutedPopup": "Das freigegebene Video wurde stumm geschaltet um mit den anderen Teilnehmern zu sprechen.",
"toggleCamera": "",
"micMutedPopup": "Das Mikrofon wurde stumm geschaltet um das freigegebene Video geniessen zu können.",
"talkWhileMutedPopup": "Versuchen sie zu sprechen? Ihr Mikrofon ist stummgeschaltet.",
"unableToUnmutePopup": "Die Stummschaltung kann nicht aufgehoben werden während das geteilte Video abgespielt wird.",
"cameraDisabled": "Keine Kamera verfügbar",
"micDisabled": "Kein Mikrofon verfügbar",
"filmstrip": "Videos anzeigen / verbergen",
"pip": "",
"profile": "Profil bearbeiten",
"raiseHand": "Hand erheben",
"shortcuts": "Tastenkürzel anzeigen",
"speakerStats": "Sprecher-Statistiken",
"tileViewToggle": "",
"invite": "Teilnehmer einladen"
},
"\u0005toolbar": {
"accessibilityLabel": {}
"speakerStats": "Sprecher-Statistiken"
},
"chat": {
"nickname": {
"title": "Name eingeben",
"popover": "Name"
},
"error": "",
"messagebox": "Text eingeben..."
},
"settings": {
"calendar": {
"about": "",
"disconnect": "Getrennt",
"microsoftSignIn": "",
"signedIn": "",
"title": "Kalender"
},
"title": "Einstellungen",
"update": "Aktualisieren",
"name": "Name",
@@ -184,18 +124,11 @@
"selectMic": "Mikrofon",
"selectAudioOutput": "Audioausgabe",
"followMe": "Follow-me für alle Teilnehmer",
"language": "",
"loggedIn": "",
"noDevice": "Kein",
"cameraAndMic": "Kamera und Mikrofon",
"moderator": "Moderator",
"more": "",
"moderator": "MODERATOR",
"password": "PASSWORT SETZEN",
"audioVideo": "AUDIO UND VIDEO",
"devices": ""
},
"\u0005settings": {
"calendar": {}
"audioVideo": "AUDIO UND VIDEO"
},
"profile": {
"title": "Profil",
@@ -215,9 +148,7 @@
},
"connectionindicator": {
"header": "Verbindungsdaten",
"connectedTo": "",
"bitrate": "Bitrate:",
"bridgeCount": "",
"packetloss": "Paketverlust:",
"resolution": "Auflösung:",
"framerate": "Bildwiederholrate:",
@@ -256,6 +187,7 @@
"focus": "Konferenz-Organisator",
"focusFail": "__component__ ist im Moment nicht verfügbar - wiederholen in __ms__ Sekunden",
"grantedTo": "Moderatorenrechte an __to__ vergeben.",
"grantedToUnknown": "Moderatorenrechte an $t(notify.somebody) vergeben.",
"muted": "Der Konferenz wurde stumm beigetreten.",
"mutedTitle": "Stummschaltung aktiv!",
"raisedHand": "Möchte sprechen.",
@@ -263,13 +195,8 @@
"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": {
"accessibilityLabel": {
"liveStreaming": "Livestream:"
},
"allow": "Erlauben",
"confirm": "",
"kickMessage": "Oh! Sie wurden aus der Konferenz ausgeschlossen.",
"kickTitle": "",
"popupErrorTitle": "Popup blockiert",
"popupError": "Ihr Browser blockiert Popups von dieser Website. Bitte aktivieren Sie Popups in den Sicherheitseinstellungen des Browsers und versuchen Sie es erneut.",
"passwordErrorTitle": "Passwort-Fehler",
@@ -292,7 +219,6 @@
"rejoinNow": "Jetzt erneut beitreten",
"maxUsersLimitReachedTitle": "Maximale Anzahl Teilnehmer ist erreicht",
"maxUsersLimitReached": "Das Limit der maximalen Anzahl Teilnehmer wurde erreicht. Die Konferenz ist voll. Bitte benachrichtigen Sie den Konferenzorganisator oder versuchen Sie es später noch einmal.",
"lockRoom": "",
"lockTitle": "Sperren fehlgeschlagen",
"lockMessage": "Die Konferenz konnte nicht gesperrt werden.",
"warning": "Warnung",
@@ -340,7 +266,6 @@
"reservationError": "Fehler im Reservationssystem",
"reservationErrorMsg": "Fehler, Nummer: __code__, Nachricht: __msg__",
"password": "Passwort eingeben",
"unlockRoom": "",
"userPassword": "Benutzerpasswort",
"token": "Token",
"tokenAuthFailedTitle": "Authentifizierung fehlgeschlagen",
@@ -353,7 +278,7 @@
"sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?",
"liveStreaming": "Live-Streaming",
"streamKey": "Name/Schlüssel für den Stream",
"startLiveStreaming": "Einen Livestream starten",
"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?",
@@ -387,10 +312,6 @@
"muteParticipantTitle": "Diesen Teilnehmer stummschalten?",
"muteParticipantBody": "Sie können die Stummschaltung anderer Teilnehmer nicht aufheben, aber ein Teilnehmer kann seine eigene Stummschaltung jederzeit beenden.",
"muteParticipantButton": "Stummschalten",
"liveStreamingDisabledTooltip": "",
"liveStreamingDisabledForGuestTooltip": "",
"recordingDisabledTooltip": "",
"recordingDisabledForGuestTooltip": "",
"remoteControlTitle": "Fernsteuerung",
"remoteControlRequestMessage": "Möchten Sie __user__ erlauben den Computer fernzusteuern?",
"remoteControlShareScreenWarning": "Achtung, wenn Sie die Anfrage genehmigen starten Sie die Bildschirmfreigabe!",
@@ -401,15 +322,10 @@
"remoteControlStopMessage": "Die Fernsteuerung wurde beendet.",
"close": "Schliessen",
"shareYourScreen": "Bildschirm freigeben",
"shareYourScreenDisabled": "",
"shareYourScreenDisabledForGuest": "",
"yourEntireScreen": "Ganzer Bildschirm",
"applicationWindow": "Anwendungsfenster",
"transcribing": ""
},
"\u0005dialog": {
"accessibilityLabel": {}
"applicationWindow": "Anwendungsfenster"
},
"\u0005dialog": {},
"email": {
"sharedKey": [
"Diese Konferenz ist passwortgeschützt. Bitte verwenden Sie diese PIN zum Beitreten:",
@@ -441,10 +357,6 @@
],
"and": "und"
},
"share": {
"mainText": "",
"dialInfoText": ""
},
"connection": {
"ERROR": "Fehler",
"CONNECTING": "Verbindung wird hergestellt",
@@ -458,44 +370,18 @@
"ATTACHED": "Angehängt"
},
"recording": {
"beta": "",
"busy": "Es werden Resourcen für eine Aufnahme bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
"busyTitle": "Alle Aufnahme-Instanzen sind in Gebrauch",
"buttonTooltip": "Aufnahme starten / stoppen",
"error": "Die Aufzeichnung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
"expandedOff": "",
"expandedOn": "",
"expandedPending": "",
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
"live": "",
"off": "Aufnahme gestoppt",
"on": "Aufnahme",
"pending": "",
"rec": "",
"authDropboxText": "",
"pending": "Die Aufnahme wartet auf den Beitritt eines Teilnehmers...",
"serviceName": "Aufnahmedienst",
"signOut": "",
"signIn": "",
"loggedIn": "",
"availableSpace": "",
"startRecordingBody": "Sind Sie sicher dass Sie die Aufnahme stoppen möchten?",
"unavailable": "Oh! Der __serviceName__ ist aktuell nicht verfügbar. Wir arbeiten an der Behebung des Problems. Bitte versuchen Sie es später noch einmal.",
"unavailableTitle": "Aufnahme nicht verfügbar"
},
"\u0005recording": {},
"transcribing": {
"pending": "",
"off": "",
"error": "Die Aufzeichnung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
"expandedLabel": "",
"failedToStart": "",
"tr": "",
"labelToolTip": "",
"ccButtonTooltip": "",
"start": "",
"stop": ""
},
"\u0005transcribing": {},
"liveStreaming": {
"busy": "Es werden Resourcen zum Streamen bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
"busyTitle": "Alle Streaming-Instanzen sind in Gebrauch",
@@ -506,24 +392,17 @@
"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.",
"errorLiveStreamNotEnabled": "",
"expandedOff": "",
"expandedOn": "",
"expandedPending": "",
"failedToStart": "Live-Streaming konnte nicht gestartet werden",
"off": "Live-Streaming gestoppt",
"on": "Live-Streaming",
"pending": "Live-Stream wird gestartet...",
"serviceName": "Live Streaming-Dienst",
"signedInAs": "",
"signIn": "Mit Google anmelden",
"signOut": "",
"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"
},
"\u0005liveStreaming": {},
"videoSIPGW": {
"busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"busyTitle": "Keine freien Ressourcen",
@@ -549,11 +428,9 @@
"noPermission": "Berechtigungen nicht erteilt",
"previewUnavailable": "Keine Vorschau verfügbar",
"selectADevice": "Ein Gerät wählen",
"testAudio": ""
"testAudio": "Audio testen"
},
"videoStatus": {
"audioOnly": "",
"audioOnlyExpanded": "",
"callQuality": "Konferenzqualität",
"hd": "HD",
"hdTooltip": "Video wird in HD angezeigt",
@@ -581,8 +458,7 @@
"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": "Sie können keine Teilnehmer einladen.",
"footerText": "",
"disabled": "",
"invite": "Einladen",
"loading": "Suche nach Teilnehmern und Telefonnummern",
"loadingNumber": "Telefonnummer wird überprüft",
@@ -619,7 +495,6 @@
"veryGood": "Sehr gut"
},
"info": {
"accessibilityLabel": "",
"addPassword": "Passwort hinzufügen",
"cancelPassword": "Password abbrechen",
"conferenceURL": "Link:",
@@ -641,7 +516,7 @@
"numbers": "Einwählnummern",
"password": "Passwort:",
"title": "Teilen",
"tooltip": ""
"tooltip": "Zugriffsinformationen über die Konferenz abrufen"
},
"settingsView": {
"alertOk": "OK",
@@ -657,22 +532,17 @@
"startWithVideoMuted": "Ohne Video beitreten"
},
"calendarSync": {
"addMeetingURL": "",
"confirmAddLink": "",
"confirmAddLinkTitle": "Kalender",
"join": "",
"joinTooltip": "",
"later": "Später",
"next": "Folgend",
"nextMeeting": "Nächste Konferenz",
"noEvents": "",
"ongoingMeeting": "",
"now": "Jetzt",
"permissionButton": "Einstellungen öffnen",
"permissionMessage": "Die App benötigt Zugriff auf den Kalender um die Termine und Konferenzen anzuzeigen.",
"refresh": "",
"today": "Heute"
"permissionMessage": "Die App benötigt Zugriff auf den Kalender um die Termine und Konferenzen anzuzeigen."
},
"\u0005calendarSync": {},
"recentList": {
"joinPastMeeting": ""
"today": "Heute",
"yesterday": "Gestern",
"earlier": "Früher"
},
"sectionList": {
"pullToRefresh": "Ziehen um zu aktualisieren"
@@ -685,62 +555,5 @@
"appNotInstalled": "Sie benötigen die __app__ App um der Konferenz auf dem Smartphone beizutreten.",
"downloadApp": "App herunterladen",
"openApp": "In der App fortfahren"
},
"presenceStatus": {
"invited": "Einladen",
"ringing": "",
"calling": "",
"initializingCall": "",
"connected": "Verbunden",
"connecting": "Verbindung wird hergestellt",
"connecting2": "",
"disconnected": "Getrennt",
"busy": "",
"rejected": "",
"ignored": "",
"expired": ""
},
"\u0005presenceStatus": {},
"dateUtils": {
"today": "Heute",
"yesterday": "Gestern",
"earlier": "Früher"
},
"incomingCall": {
"answer": "",
"audioCallTitle": "",
"decline": "OK",
"productLabel": "",
"videoCallTitle": ""
},
"localRecording": {
"localRecording": "",
"dialogTitle": "",
"start": "Aufnahme starten",
"stop": "Aufnahme stoppen",
"moderator": "Moderator",
"me": "Ich",
"duration": "",
"durationNA": "",
"encoding": "",
"participantStats": "",
"participant": "Teilnehmer",
"sessionToken": "",
"clientState": {
"on": "",
"off": "",
"unknown": ""
},
"messages": {
"engaged": "",
"finished": "",
"finishedModerator": "",
"notModerator": ""
},
"yes": "Ja",
"no": "",
"label": "",
"labelToolTip": ""
},
"\u0005localRecording": {}
}
}

View File

@@ -1,746 +0,0 @@
{
"addPeople": {
"add": "",
"countryNotSupported": "",
"countryReminder": "",
"disabled": "",
"failedToAdd": "",
"footerText": "",
"invite": "",
"loading": "",
"loadingNumber": "",
"loadingPeople": "",
"noResults": "",
"notAvailable": "",
"noValidNumbers": "",
"searchNumbers": "",
"searchPeople": "",
"searchPeopleAndNumbers": "",
"telephone": "",
"title": ""
},
"audioDevices": {
"bluetooth": "",
"headphones": "",
"phone": "",
"speaker": ""
},
"audioOnly": {
"audioOnly": "",
"featureToggleDisabled": ""
},
"calendarSync": {
"addMeetingURL": "",
"confirmAddLink": "",
"confirmAddLinkTitle": "",
"error": {
"appConfiguration": "",
"generic": "",
"notSignedIn": ""
},
"join": "",
"joinTooltip": "",
"nextMeeting": "",
"noEvents": "",
"ongoingMeeting": "",
"permissionButton": "",
"permissionMessage": "",
"refresh": "",
"today": ""
},
"chat": {
"error": "",
"messagebox": "",
"nickname": {
"popover": "",
"title": ""
},
"title": ""
},
"connection": {
"ATTACHED": "",
"AUTHENTICATING": "",
"AUTHFAIL": "",
"CONNECTED": "",
"CONNECTING": "",
"CONNFAIL": "",
"DISCONNECTED": "",
"DISCONNECTING": "",
"ERROR": "",
"RECONNECTING": ""
},
"connectionindicator": {
"address": "",
"bandwidth": "",
"bitrate": "",
"bridgeCount": "",
"connectedTo": "",
"framerate": "",
"header": "",
"less": "",
"localaddress": "",
"localaddress_plural_2": "",
"localport": "",
"localport_plural_2": "",
"more": "",
"na": "",
"packetloss": "",
"quality": {
"good": "",
"inactive": "",
"lost": "",
"nonoptimal": "",
"poor": ""
},
"remoteaddress": "",
"remoteaddress_plural_2": "",
"remoteport": "",
"remoteport_plural_2": "",
"resolution": "",
"status": "",
"transport": "",
"transport_plural_2": "",
"turn": ""
},
"contactlist_plural": "",
"dateUtils": {
"earlier": "",
"today": "",
"yesterday": ""
},
"deepLinking": {
"appNotInstalled": "",
"description": "",
"downloadApp": "",
"launchWebButton": "",
"openApp": "",
"title": "",
"tryAgainButton": ""
},
"defaultLink": "",
"defaultNickname": "",
"deviceError": {
"cameraError": "",
"cameraPermission": "",
"microphoneError": "",
"microphonePermission": ""
},
"deviceSelection": {
"deviceSettings": "",
"noPermission": "",
"previewUnavailable": "",
"selectADevice": "",
"testAudio": ""
},
"dialog": {
"accessibilityLabel": {
"liveStreaming": ""
},
"allow": "",
"alreadySharedVideoMsg": "",
"alreadySharedVideoTitle": "",
"applicationWindow": "",
"Back": "",
"cameraConstraintFailedError": "",
"cameraNotFoundError": "",
"cameraNotSendingData": "",
"cameraNotSendingDataTitle": "",
"cameraPermissionDeniedError": "",
"cameraUnknownError": "",
"cameraUnsupportedResolutionError": "",
"Cancel": "",
"close": "",
"conferenceDisconnectMsg": "",
"conferenceDisconnectTitle": "",
"conferenceReloadMsg": "",
"conferenceReloadTitle": "",
"confirm": "",
"confirmNo": "",
"confirmYes": "",
"connectError": "",
"connectErrorWithMsg": "",
"connecting": "",
"contactSupport": "",
"copy": "",
"currentPassword": "",
"defaultError": "",
"detectext": "",
"dismiss": "",
"displayNameRequired": "",
"done": "",
"doNotShowMessageAgain": "",
"enterDisplayName": "",
"error": "",
"externalInstallationMsg": "",
"externalInstallationTitle": "",
"failedpermissions": "",
"feedbackHelp": "",
"feedbackQuestion": "",
"goToStore": "",
"gracefulShutdown": "",
"hungUp": "",
"IamHost": "",
"incorrectPassword": "",
"inlineInstallationMsg": "",
"inlineInstallExtension": "",
"internalError": "",
"internalErrorTitle": "",
"joinAgain": "",
"kickMessage": "",
"kickParticipantButton": "",
"kickParticipantDialog": "",
"kickParticipantTitle": "",
"kickTitle": "",
"liveStreaming": "",
"liveStreamingDisabledForGuestTooltip": "",
"liveStreamingDisabledTooltip": "",
"lockMessage": "",
"lockRoom": "",
"lockTitle": "",
"logoutQuestion": "",
"logoutTitle": "",
"maxUsersLimitReached": "",
"maxUsersLimitReachedTitle": "",
"micConstraintFailedError": "",
"micNotFoundError": "",
"micNotSendingData": "",
"micNotSendingDataTitle": "",
"micPermissionDeniedError": "",
"micUnknownError": "",
"muteParticipantBody": "",
"muteParticipantButton": "",
"muteParticipantDialog": "",
"muteParticipantTitle": "",
"Ok": "",
"oops": "",
"password": "",
"passwordError": "",
"passwordError2": "",
"passwordErrorTitle": "",
"passwordLabel": "",
"passwordNotSupported": "",
"passwordNotSupportedTitle": "",
"passwordRequired": "",
"permissionDenied": "",
"popupError": "",
"popupErrorTitle": "",
"recording": "",
"recordingDisabledForGuestTooltip": "",
"recordingDisabledTooltip": "",
"recordingToken": "",
"rejoinNow": "",
"remoteControlAllowedMessage": "",
"remoteControlDeniedMessage": "",
"remoteControlErrorMessage": "",
"remoteControlRequestMessage": "",
"remoteControlShareScreenWarning": "",
"remoteControlStopMessage": "",
"remoteControlTitle": "",
"Remove": "",
"removePassword": "",
"removeSharedVideoMsg": "",
"removeSharedVideoTitle": "",
"reservationError": "",
"reservationErrorMsg": "",
"retry": "",
"Save": "",
"screenSharingFailedToInstall": "",
"screenSharingFailedToInstallTitle": "",
"screenSharingFirefoxPermissionDeniedError": "",
"screenSharingFirefoxPermissionDeniedTitle": "",
"screenSharingPermissionDeniedError": "",
"serviceUnavailable": "",
"sessTerminated": "",
"Share": "",
"shareVideoLinkError": "",
"shareVideoTitle": "",
"shareYourScreen": "",
"shareYourScreenDisabled": "",
"shareYourScreenDisabledForGuest": "",
"SLDFailure": "",
"sorryFeedback": "",
"SRDFailure": "",
"startLiveStreaming": "",
"startRecording": "",
"startRemoteControlErrorMessage": "",
"stopLiveStreaming": "",
"stopRecording": "",
"stopRecordingWarning": "",
"stopStreamingWarning": "",
"streamKey": "",
"Submit": "",
"thankYou": "",
"token": "",
"tokenAuthFailed": "",
"tokenAuthFailedTitle": "",
"transcribing": "",
"unableToSwitch": "",
"unlockRoom": "",
"userPassword": "",
"WaitForHostMsg": "",
"WaitForHostMsgWOk": "",
"WaitingForHost": "",
"warning": "",
"Yes": "",
"yourEntireScreen": ""
},
"dialOut": {
"statusMessage": ""
},
"email": {
"and": "",
"body": "",
"sharedKey": "",
"subject": ""
},
"feedback": {
"average": "",
"bad": "",
"detailsLabel": "",
"good": "",
"rateExperience": "",
"veryBad": "",
"veryGood": ""
},
"incomingCall": {
"answer": "",
"audioCallTitle": "",
"decline": "",
"productLabel": "",
"videoCallTitle": ""
},
"info": {
"accessibilityLabel": "",
"addPassword": "",
"cancelPassword": "",
"conferenceURL": "",
"country": "",
"dialANumber": "",
"dialInConferenceID": "",
"dialInNotSupported": "",
"dialInNumber": "",
"dialInTollFree": "",
"genericError": "",
"inviteLiveStream": "",
"invitePhone": "",
"invitePhoneAlternatives": "",
"inviteURL": "",
"liveStreamURL": "",
"moreNumbers": "",
"noNumbers": "",
"noPassword": "",
"noRoom": "",
"numbers": "",
"password": "",
"title": "",
"tooltip": "",
"label": ""
},
"inviteDialog": {
"alertOk": "",
"alertText": "",
"alertTitle": "",
"header": "",
"searchCallOnlyPlaceholder": "",
"searchPeopleOnlyPlaceholder": "",
"searchPlaceholder": "",
"send": ""
},
"inlineDialogFailure": {
"msg": "",
"retry": "",
"support": "",
"supportMsg": ""
},
"inviteUrlDefaultMsg": "",
"keyboardShortcuts": {
"focusLocal": "",
"focusRemote": "",
"fullScreen": "",
"keyboardShortcuts": "",
"localRecording": "",
"mute": "",
"pushToTalk": "",
"raiseHand": "",
"showSpeakerStats": "",
"toggleChat": "",
"toggleFilmstrip": "",
"toggleScreensharing": "",
"toggleShortcuts": "",
"videoMute": ""
},
"liveStreaming": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"changeSignIn": "",
"choose": "",
"chooseCTA": "",
"enterStreamKey": "",
"error": "",
"errorAPI": "",
"errorLiveStreamNotEnabled": "",
"expandedOff": "",
"expandedOn": "",
"expandedPending": "",
"failedToStart": "",
"getStreamKeyManually": "",
"invalidStreamKey": "",
"off": "",
"on": "",
"pending": "",
"serviceName": "",
"signedInAs": "",
"signIn": "",
"signInCTA": "",
"signOut": "",
"start": "",
"streamIdHelp": "",
"unavailableTitle": ""
},
"localRecording": {
"clientState": {
"off": "",
"on": "",
"unknown": ""
},
"dialogTitle": "",
"duration": "",
"durationNA": "",
"encoding": "",
"label": "",
"labelToolTip": "",
"localRecording": "",
"me": "",
"messages": {
"engaged": "",
"finished": "",
"finishedModerator": "",
"notModerator": ""
},
"moderator": "",
"no": "",
"participant": "",
"participantStats": "",
"sessionToken": "",
"start": "",
"stop": "",
"yes": ""
},
"me": "",
"notify": {
"connectedOneMember": "",
"connectedThreePlusMembers": "",
"connectedTwoMembers": "",
"disconnected": "",
"focus": "",
"focusFail": "",
"grantedTo": "",
"me": "",
"moderator": "",
"muted": "",
"mutedTitle": "",
"raisedHand": "",
"somebody": "",
"suboptimalExperienceDescription": "",
"suboptimalExperienceTitle": ""
},
"passwordSetRemotely": "",
"poweredby": "",
"presenceStatus": {
"busy": "",
"calling": "",
"connected": "",
"connecting": "",
"connecting2": "",
"disconnected": "",
"expired": "",
"ignored": "",
"initializingCall": "",
"invited": "",
"rejected": "",
"ringing": ""
},
"profile": {
"setDisplayNameLabel": "",
"setEmailInput": "",
"setEmailLabel": "",
"title": ""
},
"raisedHand": "",
"recentList": {
"joinPastMeeting": ""
},
"recording": {
"authDropboxText": "",
"availableSpace": "",
"beta": "",
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"error": "",
"expandedOff": "",
"expandedOn": "",
"expandedPending": "",
"failedToStart": "",
"live": "",
"loggedIn": "",
"off": "",
"on": "",
"pending": "",
"rec": "",
"serviceDescription": "",
"serviceName": "",
"signIn": "",
"signOut": "",
"startRecordingBody": "",
"unavailable": "",
"unavailableTitle": ""
},
"sectionList": {
"pullToRefresh": ""
},
"settings": {
"audioVideo": "",
"calendar": {
"about": "",
"disconnect": "",
"microsoftSignIn": "",
"signedIn": "",
"title": ""
},
"cameraAndMic": "",
"devices": "",
"followMe": "",
"language": "",
"loggedIn": "",
"moderator": "",
"more": "",
"name": "",
"noDevice": "",
"password": "",
"selectAudioOutput": "",
"selectCamera": "",
"selectMic": "",
"startAudioMuted": "",
"startVideoMuted": "",
"title": "",
"update": ""
},
"settingsView": {
"alertOk": "",
"alertTitle": "",
"alertURLText": "",
"conferenceSection": "",
"displayName": "",
"email": "",
"header": "",
"profileSection": "",
"serverURL": "",
"startWithAudioMuted": "",
"startWithVideoMuted": ""
},
"share": {
"dialInfoText": "",
"mainText": ""
},
"speaker": "",
"speakerStats": {
"hours": "",
"minutes": "",
"name": "",
"seconds": "",
"speakerStats": "",
"speakerTime": ""
},
"startupoverlay": {
"policyText": "",
"title": ""
},
"suspendedoverlay": {
"rejoinKeyTitle": "",
"text": "",
"title": ""
},
"toolbar": {
"accessibilityLabel": {
"audioOnly": "",
"audioRoute": "",
"callQuality": "",
"cc": "",
"chat": "",
"document": "",
"feedback": "",
"fullScreen": "",
"hangup": "",
"invite": "",
"kick": "",
"localRecording": "",
"lockRoom": "",
"moreActions": "",
"moreActionsMenu": "",
"mute": "",
"pip": "",
"profile": "",
"raiseHand": "",
"recording": "",
"remoteMute": "",
"Settings": "",
"sharedvideo": "",
"shareRoom": "",
"shareYourScreen": "",
"shortcuts": "",
"speakerStats": "",
"tileView": "",
"toggleCamera": "",
"videomute": ""
},
"addPeople": "",
"audioonly": "",
"audioOnlyOff": "",
"audioOnlyOn": "",
"audioRoute": "",
"authenticate": "",
"callQuality": "",
"cameraDisabled": "",
"chat": "",
"closeChat": "",
"documentClose": "",
"documentOpen": "",
"enterFullScreen": "",
"enterTileView": "",
"etherpad": "",
"exitFullScreen": "",
"exitTileView": "",
"feedback": "",
"filmstrip": "",
"fullscreen": "",
"hangup": "",
"invite": "",
"lock": "",
"login": "",
"logout": "",
"lowerYourHand": "",
"micDisabled": "",
"micMutedPopup": "",
"moreActions": "",
"mute": "",
"openChat": "",
"pip": "",
"profile": "",
"raiseHand": "",
"raiseYourHand": "",
"Settings": "",
"sharedvideo": "",
"sharedVideoMutedPopup": "",
"shareRoom": "",
"shortcuts": "",
"sip": "",
"speakerStats": "",
"startScreenSharing": "",
"startSubtitles": "",
"stopScreenSharing": "",
"stopSubtitles": "",
"stopSharedVideo": "",
"talkWhileMutedPopup": "",
"tileViewToggle": "",
"toggleCamera": "",
"unableToUnmutePopup": "",
"videomute": ""
},
"transcribing": {
"ccButtonTooltip": "",
"error": "",
"expandedLabel": "",
"failedToStart": "",
"labelToolTip": "",
"off": "",
"pending": "",
"start": "",
"stop": "",
"tr": ""
},
"userMedia": {
"androidGrantPermissions": "",
"chromeGrantPermissions": "",
"edgeGrantPermissions": "",
"electronGrantPermissions": "",
"firefoxGrantPermissions": "",
"iexplorerGrantPermissions": "",
"nwjsGrantPermissions": "",
"operaGrantPermissions": "",
"react-nativeGrantPermissions": "",
"safariGrantPermissions": ""
},
"videoSIPGW": {
"busy": "",
"busyTitle": "",
"errorAlreadyInvited": "",
"errorInvite": "",
"errorInviteFailed": "",
"errorInviteFailedTitle": "",
"errorInviteTitle": "",
"pending": "",
"serviceName": "",
"unavailableTitle": ""
},
"videoStatus": {
"audioOnly": "",
"audioOnlyExpanded": "",
"callQuality": "",
"hd": "",
"hdTooltip": "",
"highDefinition": "",
"labelTooiltipNoVideo": "",
"labelTooltipAudioOnly": "",
"labelTooltipVideo": "",
"ld": "",
"ldTooltip": "",
"lowDefinition": "",
"onlyAudioAvailable": "",
"onlyAudioSupported": "",
"p2pEnabled": "",
"p2pVideoQualityDescription": "",
"qualityButtonTip": "",
"recHighDefinitionOnly": "",
"sd": "",
"sdTooltip": "",
"standardDefinition": ""
},
"videothumbnail": {
"domute": "",
"flip": "",
"kick": "",
"moderator": "",
"mute": "",
"muted": "",
"remoteControl": "",
"videomute": ""
},
"welcomepage": {
"accessibilityLabel": {
"join": "",
"roomname": ""
},
"appDescription": "",
"audioVideoSwitch": {
"audio": "",
"video": ""
},
"calendar": "",
"connectCalendarButton": "",
"connectCalendarText": "",
"enterRoomTitle": "",
"go": "",
"join": "",
"privacy": "",
"recentList": "",
"recentListDelete": "",
"recentListEmpty": "",
"roomname": "",
"roomnameHint": "",
"sendFeedback": "",
"terms": "",
"title": ""
}
}

View File

@@ -50,7 +50,7 @@
},
"chat": {
"error": "Error: your message \"__originalText__\" was not sent. Reason: __error__",
"messagebox": "Type a message",
"messagebox": "Enter text...",
"nickname": {
"popover": "Choose a nickname",
"title": "Enter a nickname to use chat"
@@ -351,15 +351,12 @@
"dialInConferenceID": "PIN:",
"dialInNotSupported": "Sorry, dialing in is currently not supported.",
"dialInNumber": "Dial-in:",
"dialInSummaryError": "Error fetching dial-in info now. Please try again later.",
"dialInTollFree": "Toll Free",
"genericError": "Whoops, something went wrong.",
"inviteLiveStream": "To view the live stream of this meeting, click this link: __url__",
"invitePhone": "One tap audio Dial In: __number__,,__conferenceID__#",
"invitePhoneAlternatives": "Looking for a different dial in number? Please see: __url__",
"inviteURLFirstPartGeneral": "You are invited to join a meeting.",
"inviteURLFirstPartPersonal": "__name__ is inviting you to a meeting.",
"inviteURLSecondPart": "\n__moreInfo__\nJoin meeting: __url__\n",
"inviteURL": "You are invited to join a meeting.\n__moreInfo__\nJoin meeting: __url__\n",
"inviteURLMoreInfo": "Meeting ID: __conferenceID__#\n",
"liveStreamURL": "Live stream:",
"moreNumbers": "More numbers",
@@ -481,11 +478,11 @@
"suboptimalExperienceDescription": "Eer... we are afraid your experience with __appName__ isn't going to be that great here. We are looking for ways to improve this but, until then, please try using one of the <a href='static/recommendedBrowsers.html' target='_blank'>fully supported browsers</a>.",
"suboptimalExperienceTitle": "Browser Warning",
"newDeviceCameraTitle": "New camera detected",
"newDeviceAudioTitle": "New audio device detected",
"newDeviceMicTitle": "New microphone detected",
"newDeviceCameraTitle": "New audio output detected",
"newDeviceAction": "Use"
},
"passwordSetRemotely": "set by another member",
"passwordDigitsOnly": "Up to __number__ digits",
"poweredby": "powered by",
"presenceStatus": {
"busy": "Busy",
@@ -776,7 +773,6 @@
"enterRoomTitle": "Start a new meeting",
"go": "GO",
"join": "JOIN",
"info": "Info",
"privacy": "Privacy",
"recentList": "Recent",
"recentListDelete": "Delete",

View File

@@ -8,7 +8,6 @@ import {
import { setSubject } from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import { invite } from '../../react/features/invite';
import { toggleTileView } from '../../react/features/video-layout';
import { getJitsiMeetTransport } from '../transport';
import { API_ID } from './constants';
@@ -98,11 +97,6 @@ function initCommands() {
sendAnalytics(createApiEvent('screen.sharing.toggled'));
toggleScreenSharing();
},
'toggle-tile-view': () => {
sendAnalytics(createApiEvent('tile-view.toggled'));
APP.store.dispatch(toggleTileView());
},
'video-hangup': () => {
sendAnalytics(createApiEvent('video.hangup'));
APP.conference.hangup(true);
@@ -628,21 +622,6 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that tile view has been
* entered or exited.
*
* @param {string} enabled - True if tile view is currently displayed, false
* otherwise.
* @returns {void}
*/
notifyTileViewChanged(enabled: boolean) {
this._sendEvent({
name: 'tile-view-changed',
enabled
});
}
/**
* Disposes the allocated resources.
*

View File

@@ -39,7 +39,6 @@ const commands = {
toggleChat: 'toggle-chat',
toggleFilmStrip: 'toggle-film-strip',
toggleShareScreen: 'toggle-share-screen',
toggleTileView: 'toggle-tile-view',
toggleVideo: 'toggle-video'
};
@@ -68,8 +67,7 @@ const events = {
'video-availability-changed': 'videoAvailabilityChanged',
'video-mute-status-changed': 'videoMuteStatusChanged',
'screen-sharing-status-changed': 'screenSharingStatusChanged',
'subject-change': 'subjectChange',
'tile-view-changed': 'tileViewChanged'
'subject-change': 'subjectChange'
};
/**

View File

@@ -166,6 +166,22 @@ function getCameraVideoPosition( // eslint-disable-line max-params
verticalIndent };
}
/**
* Returns an array of the video horizontal and vertical indents.
* Centers horizontally and top aligns vertically.
*
* @return an array with 2 elements, the horizontal indent and the vertical
* indent
*/
function getDesktopVideoPosition(videoWidth, videoHeight, videoSpaceWidth) {
const horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
const verticalIndent = 0;// Top aligned
return { horizontalIndent,
verticalIndent };
}
/**
* Container for user video.
*/
@@ -350,23 +366,23 @@ export class VideoContainer extends LargeContainer {
* @returns {{horizontalIndent, verticalIndent}}
*/
getVideoPosition(width, height, containerWidth, containerHeight) {
let containerWidthToUse = containerWidth;
/* eslint-enable max-params */
if (this.stream && this.isScreenSharing()) {
let availableContainerWidth = containerWidth;
if (interfaceConfig.VERTICAL_FILMSTRIP) {
containerWidthToUse -= Filmstrip.getFilmstripWidth();
availableContainerWidth -= Filmstrip.getFilmstripWidth();
}
return getCameraVideoPosition(width,
return getDesktopVideoPosition(width,
height,
containerWidthToUse,
availableContainerWidth,
containerHeight);
}
return getCameraVideoPosition(width,
height,
containerWidthToUse,
containerWidth,
containerHeight);
}

View File

@@ -1,11 +1,6 @@
/* global APP, JitsiMeetJS */
import { getAudioOutputDeviceId } from '../../react/features/base/devices';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId,
getUserSelectedOutputDeviceId
} from '../../react/features/base/settings';
/**
* Determines if currently selected audio output device should be changed after
@@ -31,7 +26,8 @@ function getNewAudioOutputDevice(newDevices) {
return 'default';
}
const preferredAudioOutputDeviceId = getUserSelectedOutputDeviceId(APP.store.getState());
const settings = APP.store.getState()['features/base/settings'];
const preferredAudioOutputDeviceId = settings.userSelectedAudioOutputDeviceId;
// if the preferred one is not the selected and is available in the new devices
// we want to use it as it was just added
@@ -53,7 +49,8 @@ function getNewAudioOutputDevice(newDevices) {
function getNewAudioInputDevice(newDevices, localAudio) {
const availableAudioInputDevices = newDevices.filter(
d => d.kind === 'audioinput');
const selectedAudioInputDeviceId = getUserSelectedMicDeviceId(APP.store.getState());
const settings = APP.store.getState()['features/base/settings'];
const selectedAudioInputDeviceId = settings.userSelectedMicDeviceId;
const selectedAudioInputDevice = availableAudioInputDevices.find(
d => d.deviceId === selectedAudioInputDeviceId);
@@ -91,7 +88,8 @@ function getNewAudioInputDevice(newDevices, localAudio) {
function getNewVideoInputDevice(newDevices, localVideo) {
const availableVideoInputDevices = newDevices.filter(
d => d.kind === 'videoinput');
const selectedVideoInputDeviceId = getUserSelectedCameraDeviceId(APP.store.getState());
const settings = APP.store.getState()['features/base/settings'];
const selectedVideoInputDeviceId = settings.userSelectedCameraDeviceId;
const selectedVideoInputDevice = availableVideoInputDevices.find(
d => d.deviceId === selectedVideoInputDeviceId);

125
package-lock.json generated
View File

@@ -2448,15 +2448,10 @@
"isomorphic-fetch": "^2.2.1"
}
},
"@react-native-community/async-storage": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@react-native-community/async-storage/-/async-storage-1.3.4.tgz",
"integrity": "sha512-fJmzL27x0BEjhmMXPnDPnUNCZK7bph+NBVCfAz9fzHzAamaiOkdUwuL3PvE4Oj4Kw4knP8ocw5VRDGorAidZ2g=="
},
"@react-native-community/cli": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-1.9.4.tgz",
"integrity": "sha512-7XjgqCdi23g6V7RV4tsYvqVqOBtNjAsWe5Oj2dR5KxDi3YqUyIyPjDWzyFkIxiO9XTGp9Al4QSmRwtOERvHO8A==",
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-1.9.2.tgz",
"integrity": "sha512-wSw3g6HrSUvLZiHiWRcO++JrKdbYNRWycGbGHVCnRLsdDRsj/y152xPlvBa29C8w+1SwiiN8aGsBOO0x9hkrCg==",
"requires": {
"chalk": "^1.1.1",
"commander": "^2.19.0",
@@ -2840,7 +2835,7 @@
"blueimp-md5": "^2.10.0",
"json3": "^3.3.2",
"lodash": "^4.17.4",
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d"
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f1"
},
"dependencies": {
"ua-parser-js": {
@@ -5485,41 +5480,12 @@
}
},
"errorhandler": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz",
"integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.0.tgz",
"integrity": "sha1-6rpkyl1UKjEayUX1gt78M2Fl2fQ=",
"requires": {
"accepts": "~1.3.7",
"accepts": "~1.3.3",
"escape-html": "~1.0.3"
},
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
}
}
},
"es-abstract": {
@@ -6750,7 +6716,8 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@@ -6768,11 +6735,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -6785,15 +6754,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -6896,7 +6868,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@@ -6906,6 +6879,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -6918,17 +6892,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -6945,6 +6922,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -7017,7 +6995,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -7027,6 +7006,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -7102,7 +7082,8 @@
},
"safe-buffer": {
"version": "5.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -7132,6 +7113,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -7149,6 +7131,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -7187,11 +7170,13 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true
"bundled": true,
"optional": true
},
"yallist": {
"version": "3.0.2",
"bundled": true
"bundled": true,
"optional": true
}
}
},
@@ -12053,9 +12038,9 @@
}
},
"react-native": {
"version": "0.59.8",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.59.8.tgz",
"integrity": "sha512-x1T+/pEXrjgdH9uDzd5doJy5aFlBqW04j7ljDKIGALchhnvdFbtXXrUZ/1PfWHMrIdZxtaDt4tkSttp662GSQA==",
"version": "0.59.5",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.59.5.tgz",
"integrity": "sha512-8Q/9cS6IMsGNiFhJgzmncbUeuacXQMe5EJl0c63fW30DvjEjeTVCvhM08eGzSpsNlOvL2XDRa4YOiCrwI7S1TA==",
"requires": {
"@babel/runtime": "^7.0.0",
"@react-native-community/cli": "^1.2.1",
@@ -12289,25 +12274,6 @@
"prop-types": "^15.5.10"
}
},
"react-native-webview": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-5.8.1.tgz",
"integrity": "sha512-b6pSvmjoiWtcz6YspggW02X+BRXJWuquHwkh37BRx1NMW1iwMZA31SnFQvTpPzWYYIb9WF/mRsy2nGtt9C6NIg==",
"requires": {
"escape-string-regexp": "1.0.5",
"invariant": "2.2.4"
},
"dependencies": {
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"requires": {
"loose-envify": "^1.0.0"
}
}
}
},
"react-node-resolver": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/react-node-resolver/-/react-node-resolver-1.0.1.tgz",
@@ -12379,15 +12345,6 @@
"exenv": "^1.2.2"
}
},
"react-textarea-autosize": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.0.tgz",
"integrity": "sha512-c2FlR/fP0qbxmlrW96SdrbgP/v0XZMTupqB90zybvmDVDutytUgPl7beU35klwcTeMepUIQEpQUn3P3bdshGPg==",
"requires": {
"@babel/runtime": "^7.1.2",
"prop-types": "^15.6.0"
}
},
"react-transform-hmr": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/react-transform-hmr/-/react-transform-hmr-1.0.4.tgz",

View File

@@ -34,7 +34,6 @@
"@atlaskit/toggle": "5.0.14",
"@atlaskit/tooltip": "12.1.13",
"@microsoft/microsoft-graph-client": "1.1.0",
"@react-native-community/async-storage": "1.3.4",
"@webcomponents/url": "0.7.1",
"amplitude-js": "4.5.2",
"bc-css-flags": "3.0.0",
@@ -63,7 +62,7 @@
"react-emoji-render": "0.4.6",
"react-i18next": "7.13.0",
"react-linkify": "0.2.2",
"react-native": "0.59.8",
"react-native": "0.59.5",
"react-native-background-timer": "2.1.1",
"react-native-calendar-events": "1.6.4",
"react-native-callstats": "3.58.2",
@@ -77,9 +76,7 @@
"react-native-vector-icons": "6.0.2",
"react-native-watch-connectivity": "0.2.0",
"react-native-webrtc": "github:jitsi/react-native-webrtc#4064c6f2db4f8b961daaaa8dafc6a896d7cfbc43",
"react-native-webview": "5.8.1",
"react-redux": "5.0.7",
"react-textarea-autosize": "7.1.0",
"react-transition-group": "2.4.0",
"redux": "4.0.0",
"redux-thunk": "2.2.0",

View File

@@ -2,6 +2,7 @@
import type { Dispatch } from 'redux';
import { appWillNavigate } from '../base/app';
import { setRoom } from '../base/conference';
import {
configWillLoad,
@@ -10,9 +11,8 @@ import {
setConfig,
storeConfig
} from '../base/config';
import { connect, disconnect, setLocationURL } from '../base/connection';
import { setLocationURL } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet';
import { createDesiredLocalTracks } from '../base/tracks';
import { parseURIString, toURLString } from '../base/util';
import { setFatalError } from '../overlay';
@@ -59,11 +59,8 @@ export function appNavigate(uri: ?string) {
const { contextRoot, host, room } = location;
const locationURL = new URL(location.toString());
// Disconnect from any current conference.
// FIXME: unify with web.
if (navigator.product === 'ReactNative') {
dispatch(disconnect());
}
// XXX this looks like CONFIG_WILL_LOAD ?
dispatch(appWillNavigate(locationURL, room));
dispatch(configWillLoad(locationURL, room));
@@ -81,40 +78,25 @@ export function appNavigate(uri: ?string) {
let config;
// Avoid (re)loading the config when there is no room.
if (!room) {
try {
config = await loadConfig(url);
dispatch(storeConfig(baseURL, config));
} catch (error) {
config = restoreConfig(baseURL);
}
if (!config) {
try {
config = await loadConfig(url);
dispatch(storeConfig(baseURL, config));
} catch (error) {
config = restoreConfig(baseURL);
if (!config) {
dispatch(loadConfigError(error, locationURL, room));
if (!config) {
dispatch(loadConfigError(error, locationURL));
return;
}
return;
}
}
if (getState()['features/base/config'].locationURL !== locationURL) {
dispatch(loadConfigError(new Error('Config no longer needed!'), locationURL));
return;
}
dispatch(setLocationURL(locationURL));
dispatch(setConfig(config));
dispatch(setRoom(room));
// FIXME: unify with web, currently the connection and track creation happens in conference.js.
if (room && navigator.product === 'ReactNative') {
dispatch(createDesiredLocalTracks());
dispatch(connect());
if (getState()['features/base/config'].locationURL === locationURL) {
dispatch(setLocationURL(locationURL));
dispatch(setConfig(config));
dispatch(setRoom(room));
} else {
dispatch(loadConfigError(new Error('Config no longer needed!'), locationURL, room));
}
};
}

View File

@@ -113,6 +113,7 @@ export function cancelWaitForOwner() {
// clients/consumers need an event.
const { authRequired } = getState()['features/base/conference'];
// FIXME remove when external-api ported to base/session
authRequired && dispatch(conferenceLeft(authRequired));
dispatch(appNavigate(undefined));

View File

@@ -64,14 +64,12 @@ class WaitForOwnerDialog extends Component<Props> {
return (
<ConfirmDialog
cancelKey = 'dialog.Cancel'
contentKey = {
{
key: 'dialog.WaitForHostMsgWOk',
params: { room }
}
}
okKey = 'dialog.Ok'
onCancel = { this._onCancel }
onSubmit = { this._onLogin } />
);

View File

@@ -9,6 +9,13 @@
*/
export const APP_WILL_MOUNT = 'APP_WILL_MOUNT';
/**
* FIXME.
*
* @type {string}
*/
export const APP_WILL_NAVIGATE = 'APP_WILL_NAVIGATE';
/**
* The type of (redux) action which signals that a specific App will unmount (in
* React terms).

View File

@@ -2,7 +2,7 @@
import type { Dispatch } from 'redux';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
import { APP_WILL_MOUNT, APP_WILL_NAVIGATE, APP_WILL_UNMOUNT } from './actionTypes';
declare var APP;
@@ -17,6 +17,11 @@ declare var APP;
*/
export function appWillMount(app: Object) {
return (dispatch: Dispatch<any>) => {
dispatch({
type: APP_WILL_MOUNT,
app
});
// TODO There was a redux action creator appInit which I did not like
// because we already had the redux action creator appWillMount and,
// respectively, the redux action APP_WILL_MOUNT. So I set out to remove
@@ -25,11 +30,25 @@ export function appWillMount(app: Object) {
// API module into its own feature yet so we're bound to work on that in
// the future.
typeof APP === 'object' && APP.API.init();
};
}
dispatch({
type: APP_WILL_MOUNT,
app
});
/**
* FIXME.
*
* @param {URL} locationURL - FIXME.
* @param {string} room - FIXME.
* @returns {{
* type: APP_WILL_NAVIGATE,
* locationURL: URL,
* room: ?string
* }}
*/
export function appWillNavigate(locationURL: URL, room: ?string) {
return {
type: APP_WILL_NAVIGATE,
locationURL,
room
};
}

View File

@@ -24,6 +24,7 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import {
conferenceFailed,
conferenceLeft,
conferenceWillLeave,
createConference,
setLastN,
@@ -36,7 +37,8 @@ import {
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY,
SET_LASTN
SET_LASTN,
SET_ROOM
} from './actionTypes';
import {
_addLocalTracksToConference,
@@ -97,6 +99,9 @@ MiddlewareRegistry.register(store => next => action => {
case SET_LASTN:
return _setLastN(store, next, action);
case SET_ROOM:
return _setRoom(store, next, action);
case TRACK_ADDED:
case TRACK_REMOVED:
return _trackAddedOrRemoved(store, next, action);
@@ -561,6 +566,51 @@ function _setReceiverVideoConstraint(conference, preferred, max) {
}
}
/**
* Notifies the feature {@code base/conference} that the redix action
* {@link SET_ROOM} is being dispatched within a specific redux store.
*
* @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_ROOM} which is being
* dispatched in the specified {@code store}.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setRoom({ dispatch, getState }, next, action) {
const result = next(action);
// By the time SET_ROOM is dispatched, base/connection's locationURL and
// base/conference's leaving should be the only conference-related sources
// of truth.
const state = getState();
const { leaving } = state['features/base/conference'];
const { locationURL } = state['features/base/connection'];
const dispatchConferenceLeft = new Set();
// Figure out which of the JitsiConferences referenced by base/conference
// have not dispatched or are not likely to dispatch CONFERENCE_FAILED and
// CONFERENCE_LEFT.
forEachConference(state, (conference, url) => {
if (conference !== leaving && url && url !== locationURL) {
dispatchConferenceLeft.add(conference);
}
return true; // All JitsiConference instances are to be examined.
});
// Dispatch CONFERENCE_LEFT for the JitsiConferences referenced by
// base/conference which have not dispatched or are not likely to dispatch
// CONFERENCE_FAILED or CONFERENCE_LEFT.
for (const conference of dispatchConferenceLeft) {
dispatch(conferenceLeft(conference));
}
return result;
}
/**
* Synchronizes local tracks from state with local tracks in JitsiConference
* instance.

View File

@@ -38,17 +38,20 @@ export function configWillLoad(locationURL: URL, room: string) {
* loading of a configuration.
* @param {URL} locationURL - The URL of the location which necessitated the
* loading of a configuration.
* @param {string} room - The name of the conference room.
* @returns {{
* type: LOAD_CONFIG_ERROR,
* error: Error,
* locationURL: URL
* locationURL: URL,
* room: string
* }}
*/
export function loadConfigError(error: Error, locationURL: URL) {
export function loadConfigError(error: Error, locationURL: URL, room: ?string) {
return {
type: LOAD_CONFIG_ERROR,
error,
locationURL
locationURL,
room
};
}

View File

@@ -3,11 +3,6 @@
import _ from 'lodash';
import type { Dispatch } from 'redux';
import {
conferenceLeft,
conferenceWillLeave,
getCurrentConference
} from '../conference';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import { parseURIString } from '../util';
@@ -20,7 +15,7 @@ import {
} from './actionTypes';
import { JITSI_CONNECTION_URL_KEY } from './constants';
const logger = require('jitsi-meet-logger').getLogger(__filename);
import type JitsiConnection from 'lib-jitsi-meet/JitsiConnection';
/**
* The error structure passed to the {@link connectionFailed} action.
@@ -90,7 +85,7 @@ export function connect(id: ?string, password: ?string) {
connection[JITSI_CONNECTION_URL_KEY] = locationURL;
dispatch(_connectionWillConnect(connection));
dispatch(connectionWillConnect(connection, locationURL));
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_DISCONNECTED,
@@ -102,7 +97,7 @@ export function connect(id: ?string, password: ?string) {
JitsiConnectionEvents.CONNECTION_FAILED,
_onConnectionFailed);
return connection.connect({
connection.connect({
id,
password
});
@@ -258,16 +253,18 @@ export function connectionFailed(
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} which will
* connect.
* @param {URL} locationURL - FIXME.
* @private
* @returns {{
* type: CONNECTION_WILL_CONNECT,
* connection: JitsiConnection
* }}
*/
function _connectionWillConnect(connection) {
export function connectionWillConnect(connection: JitsiConnection, locationURL: URL) {
return {
type: CONNECTION_WILL_CONNECT,
connection
connection,
locationURL
};
}
@@ -319,64 +316,6 @@ function _constructOptions(state) {
return options;
}
/**
* Closes connection.
*
* @returns {Function}
*/
export function disconnect() {
return (dispatch: Dispatch<any>, getState: Function): Promise<void> => {
const state = getState();
// The conference we have already joined or are joining.
const conference_ = getCurrentConference(state);
// Promise which completes when the conference has been left and the
// connection has been disconnected.
let promise;
// Leave the conference.
if (conference_) {
// In a fashion similar to JitsiConference's CONFERENCE_LEFT event
// (and the respective Redux action) which is fired after the
// conference has been left, notify the application about the
// intention to leave the conference.
dispatch(conferenceWillLeave(conference_));
promise
= conference_.leave()
.catch(error => {
logger.warn(
'JitsiConference.leave() rejected with:',
error);
// The library lib-jitsi-meet failed to make the
// JitsiConference leave. Which may be because
// JitsiConference thinks it has already left.
// Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
} else {
promise = Promise.resolve();
}
// Disconnect the connection.
const { connecting, connection } = state['features/base/connection'];
// The connection we have already connected or are connecting.
const connection_ = connection || connecting;
if (connection_) {
promise = promise.then(() => connection_.disconnect());
} else {
logger.info('No connection found while disconnecting.');
}
return promise;
};
}
/**
* Sets the location URL of the application, connecton, conference, etc.
*

View File

@@ -12,6 +12,7 @@ import { configureInitialDevices } from '../devices';
export {
connectionEstablished,
connectionFailed,
connectionWillConnect,
setLocationURL
} from './actions.native';

View File

@@ -1,8 +1,5 @@
import JitsiMeetJS from '../lib-jitsi-meet';
import {
getUserSelectedOutputDeviceId,
updateSettings
} from '../settings';
import { updateSettings } from '../settings';
import {
ADD_PENDING_DEVICE_REQUEST,
@@ -21,28 +18,6 @@ import {
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Maps the WebRTC string for device type to the keys used to store configure,
* within redux, which devices should be used by default.
*/
const DEVICE_TYPE_TO_SETTINGS_KEYS = {
audioInput: {
currentDeviceId: 'micDeviceId',
userSelectedDeviceId: 'userSelectedMicDeviceId',
userSelectedDeviceLabel: 'userSelectedMicDeviceLabel'
},
audioOutput: {
currentDeviceId: 'audioOutputDeviceId',
userSelectedDeviceId: 'userSelectedAudioOutputDeviceId',
userSelectedDeviceLabel: 'userSelectedAudioOutputDeviceLabel'
},
videoInput: {
currentDeviceId: 'audioOutputDeviceId',
userSelectedDeviceId: 'userSelectedCameraDeviceId',
userSelectedDeviceLabel: 'userSelectedCameraDeviceLabel'
}
};
/**
* Adds a pending device request.
*
@@ -92,19 +67,19 @@ export function configureInitialDevices() {
return;
}
const newSettings = {};
const devicesKeysToSettingsKeys = {
audioInput: 'micDeviceId',
audioOutput: 'audioOutputDeviceId',
videoInput: 'cameraDeviceId'
};
Object.keys(deviceLabels).forEach(key => {
const label = deviceLabels[key];
const deviceId = getDeviceIdByLabel(state, label, key);
if (deviceId) {
const settingsTranslationMap = DEVICE_TYPE_TO_SETTINGS_KEYS[key];
newSettings[settingsTranslationMap.currentDeviceId] = deviceId;
newSettings[settingsTranslationMap.userSelectedDeviceId] = deviceId;
newSettings[settingsTranslationMap.userSelectedDeviceLabel] = label;
newSettings[devicesKeysToSettingsKeys[key]] = deviceId;
}
});
@@ -116,7 +91,8 @@ export function configureInitialDevices() {
return updateSettingsPromise
.then(() => {
const userSelectedAudioOutputDeviceId = getUserSelectedOutputDeviceId(getState());
const { userSelectedAudioOutputDeviceId }
= getState()['features/base/settings'];
return setAudioOutputDeviceId(userSelectedAudioOutputDeviceId, dispatch)
.catch(ex => logger.warn(`Failed to set audio output device.

View File

@@ -67,34 +67,6 @@ export function getDeviceIdByLabel(state: Object, label: string, kind: string) {
}
}
/**
* Finds a device with a label that matches the passed id and returns its label.
*
* @param {Object} state - The redux state.
* @param {string} id - The device id.
* @param {string} kind - The type of the device. One of "audioInput",
* "audioOutput", and "videoInput". Also supported is all lowercase versions
* of the preceding types.
* @returns {string|undefined}
*/
export function getDeviceLabelById(state: Object, id: string, kind: string) {
const webrtcKindToJitsiKindTranslator = {
audioinput: 'audioInput',
audiooutput: 'audioOutput',
videoinput: 'videoInput'
};
const kindToSearch = webrtcKindToJitsiKindTranslator[kind] || kind;
const device
= (state['features/base/devices'].availableDevices[kindToSearch] || [])
.find(d => d.deviceId === id);
if (device) {
return device.label;
}
}
/**
* Returns the devices set in the URL.
*
@@ -146,29 +118,24 @@ export function groupDevicesByKind(devices: Object[]): Object {
* @param {string} newId - New audio output device id.
* @param {Function} dispatch - The Redux dispatch function.
* @param {boolean} userSelection - Whether this is a user selection update.
* @param {?string} newLabel - New audio output device label to store.
* @returns {Promise}
*/
export function setAudioOutputDeviceId(
newId: string = 'default',
dispatch: Function,
userSelection: boolean = false,
newLabel: ?string): Promise<*> {
userSelection: boolean = false): Promise<*> {
return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
.then(() => {
const newSettings = {
audioOutputDeviceId: newId,
userSelectedAudioOutputDeviceId: undefined,
userSelectedAudioOutputDeviceLabel: undefined
userSelectedAudioOutputDeviceId: undefined
};
if (userSelection) {
newSettings.userSelectedAudioOutputDeviceId = newId;
newSettings.userSelectedAudioOutputDeviceLabel = newLabel;
} else {
// a flow workaround, I needed to add 'userSelectedAudioOutputDeviceId: undefined'
delete newSettings.userSelectedAudioOutputDeviceId;
delete newSettings.userSelectedAudioOutputDeviceLabel;
}
return dispatch(updateSettings(newSettings));

View File

@@ -79,9 +79,7 @@ function _conferenceJoined({ dispatch, getState }, next, action) {
/**
* Finds a new device by comparing new and old array of devices and dispatches
* notification with the new device. For new devices with same groupId only one
* notification will be shown, this is so to avoid showing multiple notifications
* for audio input and audio output devices.
* notification with the new device.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
@@ -99,25 +97,7 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
nDevice => !oldDevices.find(
device => device.deviceId === nDevice.deviceId));
// we group devices by groupID which normally is the grouping by physical device
// plugging in headset we provide normally two device, one input and one output
// and we want to show only one notification for this physical audio device
const devicesGroupBy = onlyNewDevices.reduce((accumulated, value) => {
accumulated[value.groupId] = accumulated[value.groupId] || [];
accumulated[value.groupId].push(value);
return accumulated;
}, {});
Object.values(devicesGroupBy).forEach(devicesArray => {
if (devicesArray.length < 1) {
return;
}
// let's get the first device as a reference, we will use it for
// label and type
const newDevice = devicesArray[0];
onlyNewDevices.forEach(newDevice => {
// we want to strip any device details that are not very
// user friendly, like usb ids put in brackets at the end
@@ -135,9 +115,12 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
titleKey = 'notify.newDeviceCameraTitle';
break;
}
case 'audioinput' :
case 'audioinput': {
titleKey = 'notify.newDeviceMicTitle';
break;
}
case 'audiooutput': {
titleKey = 'notify.newDeviceAudioTitle';
titleKey = 'notify.newDeviceCameraTitle';
break;
}
}
@@ -146,7 +129,7 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
description,
titleKey,
customActionNameKey: 'notify.newDeviceAction',
customActionHandler: _useDevice.bind(undefined, store, devicesArray)
customActionHandler: _useDevice.bind(undefined, store, newDevice)
}));
});
}
@@ -156,49 +139,44 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Array<MediaDeviceInfo|InputDeviceInfo>} devices - The devices to save.
* @param {MediaDeviceInfo} device - The device to save.
* @returns {boolean} - Returns true in order notifications to be dismissed.
* @private
*/
function _useDevice({ dispatch }, devices) {
devices.forEach(device => {
switch (device.kind) {
case 'videoinput': {
dispatch(updateSettings({
userSelectedCameraDeviceId: device.deviceId,
userSelectedCameraDeviceLabel: device.label
}));
function _useDevice({ dispatch }, device) {
switch (device.kind) {
case 'videoinput': {
dispatch(updateSettings({
userSelectedCameraDeviceId: device.deviceId
}));
dispatch(setVideoInputDevice(device.deviceId));
break;
}
case 'audioinput': {
dispatch(updateSettings({
userSelectedMicDeviceId: device.deviceId,
userSelectedMicDeviceLabel: device.label
}));
dispatch(setVideoInputDevice(device.deviceId));
break;
}
case 'audioinput': {
dispatch(updateSettings({
userSelectedMicDeviceId: device.deviceId
}));
dispatch(setAudioInputDevice(device.deviceId));
break;
}
case 'audiooutput': {
setAudioOutputDeviceId(
device.deviceId,
dispatch,
true,
device.label)
.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);
});
break;
}
}
});
dispatch(setAudioInputDevice(device.deviceId));
break;
}
case 'audiooutput': {
setAudioOutputDeviceId(
device.deviceId,
dispatch,
true)
.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);
});
break;
}
}
return true;
}

View File

@@ -1,52 +0,0 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { translate } from '../../../i18n';
import { connect } from '../../../redux';
import { _abstractMapStateToProps } from '../../functions';
import { type Props as AbstractProps } from './BaseDialog';
import BaseSubmitDialog from './BaseSubmitDialog';
type Props = AbstractProps & {
/**
* Untranslated i18n key of the content to be displayed.
*
* NOTE: This dialog also adds support to Object type keys that will be
* translated using the provided params. See i18n function
* {@code translate(string, Object)} for more details.
*/
contentKey: string | { key: string, params: Object},
};
/**
* Implements an alert dialog, to simply show an error or a message, then disappear on dismiss.
*/
class AlertDialog extends BaseSubmitDialog<Props, *> {
/**
* Implements {@code BaseSubmitDialog._renderSubmittable}.
*
* @inheritdoc
*/
_renderSubmittable() {
const { _dialogStyles, contentKey, t } = this.props;
const content
= typeof contentKey === 'string'
? t(contentKey)
: this._renderHTML(t(contentKey.key, contentKey.params));
return (
<Text style = { _dialogStyles.text }>
{ content }
</Text>
);
}
_renderHTML: string => Object | string
}
export default translate(connect(_abstractMapStateToProps)(AlertDialog));

View File

@@ -1,7 +1,7 @@
// @flow
import React, { PureComponent, type Node } from 'react';
import { Platform, SafeAreaView, ScrollView, View } from 'react-native';
import { SafeAreaView, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { SlidingView } from '../../../react';
@@ -36,6 +36,21 @@ type Props = {
* A component emulating Android's BottomSheet.
*/
class BottomSheet extends PureComponent<Props> {
/**
* Assembles a style for the BottomSheet container.
*
* @private
* @returns {StyleType}
*/
_getContainerStyle() {
const { _styles } = this.props;
return {
...styles.container,
backgroundColor: _styles.sheet.backgroundColor
};
}
/**
* Implements React's {@link Component#render()}.
*
@@ -50,46 +65,15 @@ class BottomSheet extends PureComponent<Props> {
onHide = { this.props.onCancel }
position = 'bottom'
show = { true }>
<View
pointerEvents = 'box-none'
style = { styles.sheetContainer }>
<View
pointerEvents = 'box-none'
style = { styles.sheetAreaCover } />
<View
style = { [
styles.sheetItemContainer,
_styles.sheet
] }>
<ScrollView bounces = { false }>
{ this._getWrappedContent() }
</ScrollView>
<SafeAreaView
style = { this._getContainerStyle() }>
<View style = { _styles.sheet }>
{ this.props.children }
</View>
</View>
</SafeAreaView>
</SlidingView>
);
}
/**
* Wraps the content when needed (iOS 11 and above), or just returns the original children.
*
* @returns {React$Element}
*/
_getWrappedContent() {
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS > 10) {
return (
<SafeAreaView>
{ this.props.children }
</SafeAreaView>
);
}
}
return this.props.children;
}
}
/**

View File

@@ -32,12 +32,7 @@ type Props = BaseProps & {
t: Function,
textInputProps: ?Object,
/**
* Validating of the input.
*/
validateInput: ?Function
textInputProps: ?Object
}
type State = {
@@ -123,12 +118,6 @@ class InputDialog extends BaseDialog<Props, State> {
* @returns {void}
*/
_onChangeText(fieldValue) {
if (this.props.validateInput
&& !this.props.validateInput(fieldValue)) {
return;
}
this.setState({
fieldValue
});

View File

@@ -4,7 +4,6 @@ export { default as BottomSheet } from './BottomSheet';
export { default as ConfirmDialog } from './ConfirmDialog';
export { default as CustomDialog } from './CustomDialog';
export { default as DialogContainer } from './DialogContainer';
export { default as AlertDialog } from './AlertDialog';
export { default as InputDialog } from './InputDialog';
export { default as CustomSubmitDialog } from './CustomSubmitDialog';

View File

@@ -27,28 +27,17 @@ export const PLACEHOLDER_COLOR = ColorPalette.lightGrey;
* been implemented as per the Material Design guidelines:
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
*/
export const bottomSheetStyles = {
sheetAreaCover: {
backgroundColor: ColorPalette.transparent,
flex: 1
},
export const bottomSheetStyles = createStyleSheet({
/**
* Style for the container of the sheet.
*/
sheetContainer: {
alignItems: 'stretch',
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-end'
},
sheetItemContainer: {
flex: -1,
maxHeight: '60%',
paddingHorizontal: MD_ITEM_MARGIN_PADDING
container: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0
}
};
});
export const brandedDialog = createStyleSheet({
@@ -148,7 +137,10 @@ ColorSchemeRegistry.register('BottomSheet', {
* Bottom sheet's base style.
*/
sheet: {
backgroundColor: schemeColor('background')
backgroundColor: schemeColor('background'),
flex: 1,
paddingHorizontal: MD_ITEM_MARGIN_PADDING,
paddingVertical: 8
},
/**

View File

@@ -2,8 +2,6 @@
import { Component } from 'react';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Describes audio element interface used in the base/media feature for audio
* playback.
@@ -12,7 +10,7 @@ export type AudioElement = {
currentTime: number,
pause: () => void,
play: () => void,
setSinkId?: string => Function,
setSinkId?: string => void,
stop: () => void
};
@@ -115,8 +113,7 @@ export default class AbstractAudio extends Component<Props> {
setSinkId(sinkId: string): void {
this._audioElementImpl
&& typeof this._audioElementImpl.setSinkId === 'function'
&& this._audioElementImpl.setSinkId(sinkId)
.catch(error => logger.error('Error setting sink', error));
&& this._audioElementImpl.setSinkId(sinkId);
}
/**

View File

@@ -260,37 +260,15 @@ function _getAllParticipants(stateful) {
: toState(stateful)['features/base/participants'] || []);
}
/**
* Returns true if all of the meeting participants are moderators.
*
* @param {Object|Function} stateful -Object or function that can be resolved
* to the Redux state.
* @returns {boolean}
*/
export function isEveryoneModerator(stateful: Object | Function) {
const participants = _getAllParticipants(stateful);
for (const participant of participants) {
if (participant.role !== PARTICIPANT_ROLE.MODERATOR) {
return false;
}
}
return true;
}
/**
* Returns true if the current local participant is a moderator in the
* conference.
*
* @param {Object|Function} stateful - Object or function that can be resolved
* to the Redux state.
* @param {?boolean} ignoreToken - When true we ignore the token check.
* @returns {boolean}
*/
export function isLocalParticipantModerator(
stateful: Object | Function,
ignoreToken: ?boolean = false) {
export function isLocalParticipantModerator(stateful: Object | Function) {
const state = toState(stateful);
const localParticipant = getLocalParticipant(state);
@@ -300,8 +278,7 @@ export function isLocalParticipantModerator(
return (
localParticipant.role === PARTICIPANT_ROLE.MODERATOR
&& (ignoreToken
|| !state['features/base/config'].enableUserRolesBasedOnToken
&& (!state['features/base/config'].enableUserRolesBasedOnToken
|| !state['features/base/jwt'].isGuest));
}

View File

@@ -1,66 +0,0 @@
// @flow
import React, { Component } from 'react';
import { translate } from '../../../i18n';
import BackButton from './BackButton';
import ForwardButton from './ForwardButton';
import Header from './Header';
import HeaderLabel from './HeaderLabel';
type Props = {
/**
* Boolean to set the forward button disabled.
*/
forwardDisabled: boolean,
/**
* The i18n key of the the forward button label.
*/
forwardLabelKey: ?string,
/**
* The i18n key of the header label (title)
*/
headerLabelKey: ?string,
/**
* Callback to be invoked on pressing the back button.
*/
onPressBack: ?Function,
/**
* Callback to be invoked on pressing the forward button.
*/
onPressForward: ?Function,
}
/**
* Implements a header with the standard navigation content.
*/
class HeaderWithNavigation extends Component<Props> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const { onPressBack, onPressForward } = this.props;
return (
<Header>
{ onPressBack && <BackButton onPress = { onPressBack } /> }
<HeaderLabel labelKey = { this.props.headerLabelKey } />
{ onPressForward && <ForwardButton
disabled = { this.props.forwardDisabled }
labelKey = { this.props.forwardLabelKey }
onPress = { onPressForward } /> }
</Header>
);
}
}
export default translate(HeaderWithNavigation);

View File

@@ -5,11 +5,12 @@ import Swipeout from 'react-native-swipeout';
import { ColorPalette } from '../../../styles';
import Container from './Container';
import Text from './Text';
import styles from './styles';
import type { Item } from '../../Types';
import AvatarListItem from './AvatarListItem';
import Text from './Text';
import styles from './styles';
type Props = {
@@ -92,6 +93,24 @@ export default class NavigateSectionListItem extends Component<Props> {
return lines && lines.length ? lines.map(this._renderItemLine) : null;
}
/**
* Renders the secondary action label.
*
* @private
* @returns {React$Node}
*/
_renderSecondaryAction() {
const { secondaryAction } = this.props;
return (
<Container
onClick = { secondaryAction }
style = { styles.secondaryActionContainer }>
<Text style = { styles.secondaryActionLabel }>+</Text>
</Container>
);
}
/**
* Renders the content of this component.
*
@@ -119,12 +138,14 @@ export default class NavigateSectionListItem extends Component<Props> {
return (
<Swipeout
autoClose = { true }
backgroundColor = { ColorPalette.transparent }
right = { right }>
<AvatarListItem
item = { item }
onPress = { this.props.onPress } />
onPress = { this.props.onPress }>
{ this.props.secondaryAction
&& this._renderSecondaryAction() }
</AvatarListItem>
</Swipeout>
);
}

View File

@@ -8,7 +8,6 @@ export { default as Container } from './Container';
export { default as ForwardButton } from './ForwardButton';
export { default as Header } from './Header';
export { default as HeaderLabel } from './HeaderLabel';
export { default as HeaderWithNavigation } from './HeaderWithNavigation';
export { default as Image } from './Image';
export { default as Link } from './Link';
export { default as LoadingIndicator } from './LoadingIndicator';

View File

@@ -4,6 +4,7 @@ import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles';
const AVATAR_OPACITY = 0.4;
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
const SECONDARY_ACTION_BUTTON_SIZE = 30;
export const AVATAR_SIZE = 65;
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
@@ -217,6 +218,21 @@ const SECTION_LIST_STYLES = {
color: OVERLAY_FONT_COLOR
},
secondaryActionContainer: {
alignItems: 'center',
backgroundColor: ColorPalette.blue,
borderRadius: 3,
height: SECONDARY_ACTION_BUTTON_SIZE,
justifyContent: 'center',
margin: BoxModel.margin * 0.5,
marginRight: BoxModel.margin,
width: SECONDARY_ACTION_BUTTON_SIZE
},
secondaryActionLabel: {
color: ColorPalette.white
},
touchableView: {
flexDirection: 'row'
}

View File

@@ -0,0 +1,86 @@
// @flow
import uuid from 'uuid';
import { toURLString } from '../util';
import type JitsiConference from 'lib-jitsi-meet/JitsiConference';
import type JitsiConnection from 'lib-jitsi-meet/JitsiConnection';
/**
* FIXME.
*/
export class Session {
_conference: ?JitsiConference;
_connection: ?JitsiConnection;
conferenceFailed: boolean;
id: string;
locationURL: URL;
room: string;
/**
* FIXME.
*
* @param {URL} locationURL - FIXME.
* @param {string} room - FIXME.
*/
constructor(locationURL: URL, room: string) {
this.locationURL = locationURL;
this.room = room;
this.id = uuid.v4().toUpperCase();
this.conferenceFailed = false;
}
/**
* FIXME.
*
* @param {JitsiConference} [conference] - FIXME.
*/
set conference(conference: ?JitsiConference) {
if (this._conference && conference && this._conference !== conference) {
throw new Error(`Attempt to reassign conference to ${this.toString()}`);
}
this._conference = conference;
}
/**
* FIXME.
*
* @returns {?JitsiConference}
*/
get conference(): ?JitsiConference {
return this._conference;
}
/**
* FIXME.
*
* @param {JitsiConnection} [connection] - FIXME.
*/
set connection(connection: ?JitsiConnection) {
if (this._connection && connection && this._connection !== connection) {
throw new Error(`Attempt to reassign connection to ${this.toString()}`);
}
this._connection = connection;
}
/**
* FIXME.
*
* @returns {?JitsiConnection}
*/
get connection() {
return this._connection;
}
/**
* FIXME.
*
* @returns {string}
*/
toString() {
return `Session[id=${this.id}, URL: ${toURLString(this.locationURL)} room: ${this.room}]`;
}
}

View File

@@ -0,0 +1,8 @@
export const SESSION_CREATED = 'SESSION_CREATED';
export const SESSION_STARTED = 'SESSION_STARTED';
export const SESSION_TERMINATED = 'SESSION_TERMINATED';
export const SESSION_FAILED = 'SESSION_FAILED';

View File

@@ -0,0 +1,135 @@
// @flow
import { conferenceLeft, conferenceWillLeave } from '../conference';
import { SESSION_CREATED, SESSION_FAILED, SESSION_STARTED, SESSION_TERMINATED } from './actionTypes';
import { Session } from './Session';
import type { Dispatch } from 'redux';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* FIXME.
*
* @returns {Function}
*/
export function endAllSessions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const sessions = getState()['features/base/session'];
for (const session of sessions.values()) {
dispatch(endSession(session));
}
};
}
/**
* FIXME.
*
* @param {Session} session - FIXME.
* @returns {function(*): *}
*/
export function endSession(session: Session) {
return (dispatch: Dispatch<any>) => {
// The conference we have already joined or are joining.
const conference_ = session.conference;
// Promise which completes when the conference has been left and the connection has been disconnected.
let promise;
// Leave the conference.
if (conference_) {
// In a fashion similar to JitsiConference's CONFERENCE_LEFT event (and the respective Redux action) which
// is fired after the conference has been left, notify the application about the intention to leave
// the conference.
dispatch(conferenceWillLeave(conference_));
promise
= conference_.leave()
.catch(error => {
logger.warn('JitsiConference.leave() rejected with:', error);
// The library lib-jitsi-meet failed to make the JitsiConference leave. Which may be because
// JitsiConference thinks it has already left. Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
} else {
promise = Promise.resolve();
}
const connection_ = session.connection;
if (connection_) {
promise = promise.then(() => connection_.disconnect());
}
return promise;
};
}
/**
* FIXME.
*
* @param {URL} locationURL - FIXME.
* @param {string} room - FIXME.
* @returns {{
* type: SESSION_CREATED,
* session: Session
* }}
*/
export function createSession(locationURL: URL, room: string) {
return {
type: SESSION_CREATED,
session: new Session(locationURL, room)
};
}
/**
* FIXME.
*
* @param {Session} session - FIXME.
* @returns {{
* type: SESSION_FAILED,
* session: Session
* }}
*/
export function sessionFailed(session: Session) {
return {
type: SESSION_FAILED,
session
};
}
/**
* FIXME.
*
* @param {Session} session - FIXME.
* @returns {{
* session: Session,
* type: string
* }}
*/
export function sessionStarted(session: Session) {
return {
type: SESSION_STARTED,
session
};
}
/**
* FIXME.
*
* @param {Session} session - FIXME.
* @returns {{
* type: SESSION_TERMINATED,
* session: Session
* }}
*/
export function sessionTerminated(session: Session) {
return {
type: SESSION_TERMINATED,
session
};
}

View File

@@ -0,0 +1,6 @@
export * from './actions';
export * from './actionTypes';
export * from './selectors';
import './middleware';
import './reducer';

View File

@@ -0,0 +1,164 @@
import { APP_WILL_NAVIGATE } from '../app';
import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_LEFT, CONFERENCE_WILL_JOIN, SET_ROOM } from '../conference';
import { LOAD_CONFIG_ERROR } from '../config';
import { connect, CONNECTION_DISCONNECTED, CONNECTION_FAILED, CONNECTION_WILL_CONNECT } from '../connection';
import { MiddlewareRegistry } from '../redux';
import { createDesiredLocalTracks } from '../tracks';
import { toURLString } from '../util';
import { createSession, endAllSessions, endSession, sessionFailed, sessionStarted, sessionTerminated } from './actions';
import { SESSION_CREATED, SESSION_FAILED, SESSION_STARTED, SESSION_TERMINATED } from './actionTypes';
import { findSessionForConference, findSessionForConnection, findSessionForLocationURL } from './selectors';
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_NAVIGATE: {
const { locationURL, room } = action;
// Currently only one session is allowed at a time
dispatch(endAllSessions());
// Start a new session if there's a conference room name defined
room && room.length && dispatch(createSession(locationURL, room));
break;
}
case CONFERENCE_FAILED: {
const { conference, error } = action;
const { recoverable } = error;
const session = findSessionForConference(getState(), conference);
if (session) {
session.conference = null;
if (typeof recoverable === 'undefined' || recoverable === false) {
session.conferenceFailed = true;
if (session.connection) {
// The sessionFailed is expected to be dispatched from either CONNECTION_DISCONNECTED
// or CONNECTION_FAILED handler in this middleware. The purpose is to delay the SESSION_FAILED until
// the XMPP connection is properly disposed.
dispatch(endSession(session));
} else {
dispatch(sessionFailed(session));
}
}
} else {
console.error('CONFERENCE_FAILED - no session found');
}
break;
}
case CONFERENCE_LEFT: {
const { conference } = action;
const session = findSessionForConference(getState(), conference);
if (session) {
session.conference = null;
// If there's any existing connection wait for it to be closed first
session.connection || dispatch(sessionTerminated(session));
} else {
console.error('CONFERENCE_LEFT - no session found');
}
break;
}
case CONFERENCE_JOINED: {
const { conference } = action;
const session = findSessionForConference(getState(), conference);
if (session) {
dispatch(sessionStarted(session));
} else {
console.error('CONFERENCE_JOINED - no session found');
}
break;
}
case CONFERENCE_WILL_JOIN: {
const { conference } = action;
const { connection } = conference;
const session = findSessionForConnection(getState(), connection);
if (session) {
session.conference = conference;
} else {
console.error('CONFERENCE_WILL_JOIN - no session found');
}
break;
}
case LOAD_CONFIG_ERROR: {
const { locationURL, room } = action;
// There won't be a session if there's no room (it happen when the config is loaded for the welcome page).
if (room) {
const session = findSessionForLocationURL(getState(), locationURL);
if (session) {
dispatch(sessionFailed(session));
} else {
console.error(`LOAD_CONFIG_ERROR - no session found for: ${toURLString(locationURL)}`);
}
}
break;
}
case CONNECTION_DISCONNECTED: {
const { connection } = action;
const session = findSessionForConnection(getState(), connection);
if (session) {
session.connection = null;
dispatch(
session.conferenceFailed ? sessionFailed(session) : sessionTerminated(session));
} else {
console.error('CONNECTION_DISCONNECTED - no session found');
}
break;
}
case CONNECTION_FAILED: {
const { connection, error } = action;
const { recoverable } = error;
const session = findSessionForConnection(getState(), connection);
if (session) {
session.connection = null;
if (typeof recoverable === 'undefined' || recoverable === false) {
dispatch(sessionFailed(session));
}
} else {
console.error('CONNECTION_FAILED - no session found');
}
break;
}
case CONNECTION_WILL_CONNECT: {
const { connection, locationURL } = action;
const session = findSessionForLocationURL(getState(), locationURL);
if (session) {
session.connection = connection;
} else {
console.error(`CONNECTION_WILL_CONNECT - no session found for: ${toURLString(locationURL)}`);
}
break;
}
case SET_ROOM: {
const { locationURL } = getState()['features/base/connection'];
const session = findSessionForLocationURL(getState(), locationURL);
// Web has different logic for creating the local tracks and starting the connection
if (session && typeof APP === 'undefined') {
dispatch(createDesiredLocalTracks());
dispatch(connect());
}
break;
}
case SESSION_CREATED:
case SESSION_STARTED:
case SESSION_FAILED:
case SESSION_TERMINATED:
console.info(`DEBUG ${action.type} ${action.session}`);
break;
}
return result;
});

View File

@@ -0,0 +1,27 @@
import { ReducerRegistry } from '../redux';
import { SESSION_CREATED, SESSION_FAILED, SESSION_TERMINATED } from './actionTypes';
ReducerRegistry.register('features/base/session', (state = new Map(), action) => {
switch (action.type) {
case SESSION_CREATED: {
const { session } = action;
const nextMap = new Map(state);
nextMap.set(session.id, session);
return nextMap;
}
case SESSION_TERMINATED:
case SESSION_FAILED: {
const { session } = action;
const nextMap = new Map(state);
nextMap.delete(session.id);
return nextMap;
}
}
return state;
});

View File

@@ -0,0 +1,56 @@
/**
* FIXME.
*
* @param {Object} state - FIXME.
* @param {JitsiConnection} connection - FIXME.
* @returns {Session|null}
*/
export function findSessionForConnection(state, connection) {
const sessions = state['features/base/session'];
for (const session of sessions.values()) {
if (session.connection === connection) {
return session;
}
}
return null;
}
/**
* FIXME.
*
* @param {Object} state - FIXME.
* @param {JitsiConference} conference - FIXME.
* @returns {Session|null}
*/
export function findSessionForConference(state, conference) {
const sessions = state['features/base/session'];
for (const session of sessions.values()) {
if (session.conference === conference) {
return session;
}
}
return null;
}
/**
* FIXME.
*
* @param {Object} state - FIXME.
* @param {URL} locationURL - FIXME.
* @returns {Session|null}
*/
export function findSessionForLocationURL(state, locationURL) {
const sessions = state['features/base/session'];
for (const session of sessions.values()) {
if (session.locationURL === locationURL) {
return session;
}
}
return null;
}

View File

@@ -97,152 +97,3 @@ export function getServerURL(stateful: Object | Function) {
return state['features/base/settings'].serverURL || DEFAULT_SERVER_URL;
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred cameraDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedCameraDeviceId(stateful: Object | Function) {
const state = toState(stateful);
const {
userSelectedCameraDeviceId,
userSelectedCameraDeviceLabel
} = state['features/base/settings'];
const { videoInput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: videoInput,
// Operating systems may append " #{number}" somewhere in the label so
// find and strip that bit.
matchRegex: /\s#\d*(?!.*\s#\d*)/,
userSelectedDeviceId: userSelectedCameraDeviceId,
userSelectedDeviceLabel: userSelectedCameraDeviceLabel,
replacement: ''
});
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred micDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedMicDeviceId(stateful: Object | Function) {
const state = toState(stateful);
const {
userSelectedMicDeviceId,
userSelectedMicDeviceLabel
} = state['features/base/settings'];
const { audioInput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: audioInput,
// Operating systems may append " ({number}-" somewhere in the label so
// find and strip that bit.
matchRegex: /\s\(\d*-\s(?!.*\s\(\d*-\s)/,
userSelectedDeviceId: userSelectedMicDeviceId,
userSelectedDeviceLabel: userSelectedMicDeviceLabel,
replacement: ' ('
});
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred audioOutputDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedOutputDeviceId(stateful: Object | Function) {
const state = toState(stateful);
const {
userSelectedAudioOutputDeviceId,
userSelectedAudioOutputDeviceLabel
} = state['features/base/settings'];
const { audioOutput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: audioOutput,
matchRegex: undefined,
userSelectedDeviceId: userSelectedAudioOutputDeviceId,
userSelectedDeviceLabel: userSelectedAudioOutputDeviceLabel,
replacement: undefined
});
}
/**
* A helper function to abstract the logic for choosing which device ID to
* use. Falls back to fuzzy matching on label if a device ID match is not found.
*
* @param {Object} options - The arguments used to match find the preferred
* device ID from available devices.
* @param {Array<string>} options.availableDevices - The array of currently
* available devices to match against.
* @param {Object} options.matchRegex - The regex to use to find strings
* appended to the label by the operating system. The matches will be replaced
* with options.replacement, with the intent of matching the same device that
* might have a modified label.
* @param {string} options.userSelectedDeviceId - The device ID the participant
* prefers to use.
* @param {string} options.userSelectedDeviceLabel - The label associated with the
* device ID the participant prefers to use.
* @param {string} options.replacement - The string to use with
* options.matchRegex to remove identifies added to the label by the operating
* system.
* @private
* @returns {string} The preferred device ID to use for media.
*/
function _getUserSelectedDeviceId(options) {
const {
availableDevices,
matchRegex,
userSelectedDeviceId,
userSelectedDeviceLabel,
replacement
} = options;
// If there is no label at all, there is no need to fall back to checking
// the label for a fuzzy match.
if (!userSelectedDeviceLabel || !userSelectedDeviceId) {
return userSelectedDeviceId;
}
const foundMatchingBasedonDeviceId = availableDevices.find(
candidate => candidate.deviceId === userSelectedDeviceId);
// Prioritize matching the deviceId
if (foundMatchingBasedonDeviceId) {
return userSelectedDeviceId;
}
const strippedDeviceLabel
= matchRegex ? userSelectedDeviceLabel.replace(matchRegex, replacement)
: userSelectedDeviceLabel;
const foundMatchBasedOnLabel = availableDevices.find(candidate => {
const { label } = candidate;
if (!label) {
return false;
} else if (strippedDeviceLabel === label) {
return true;
}
const strippedCandidateLabel
= label.replace(matchRegex, replacement);
return strippedDeviceLabel === strippedCandidateLabel;
});
return foundMatchBasedOnLabel
? foundMatchBasedOnLabel.deviceId : userSelectedDeviceId;
}

View File

@@ -33,10 +33,7 @@ const DEFAULT_STATE = {
startWithVideoMuted: false,
userSelectedAudioOutputDeviceId: undefined,
userSelectedCameraDeviceId: undefined,
userSelectedMicDeviceId: undefined,
userSelectedAudioOutputDeviceLabel: undefined,
userSelectedCameraDeviceLabel: undefined,
userSelectedMicDeviceLabel: undefined
userSelectedMicDeviceId: undefined
};
const STORE_NAME = 'features/base/settings';

View File

@@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-community/async-storage';
import { AsyncStorage } from 'react-native';
/**
* A Web Sorage API implementation used for polyfilling

View File

@@ -3,10 +3,6 @@
import JitsiMeetJS, { JitsiTrackErrors, JitsiTrackEvents }
from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId
} from '../settings';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -41,13 +37,13 @@ export function createLocalTracksF(
// reliance on the global variable APP will go away.
store || (store = APP.store); // eslint-disable-line no-param-reassign
const state = store.getState();
const settings = store.getState()['features/base/settings'];
if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
cameraDeviceId = getUserSelectedCameraDeviceId(state);
cameraDeviceId = settings.userSelectedCameraDeviceId;
}
if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
micDeviceId = getUserSelectedMicDeviceId(state);
micDeviceId = settings.userSelectedMicDeviceId;
}
}

View File

@@ -2,14 +2,8 @@
import { PureComponent } from 'react';
import { getLocalizedDateFormatter } from '../../base/i18n';
import { getAvatarURLByParticipantId } from '../../base/participants';
/**
* Formatter string to display the message timestamp.
*/
const TIMESTAMP_FORMAT = 'H:mm';
/**
* The type of the React {@code Component} props of {@code AbstractChatMessage}.
*/
@@ -25,24 +19,6 @@ export type Props = {
*/
message: Object,
/**
* Whether or not the avatar image of the participant which sent the message
* should be displayed.
*/
showAvatar: boolean,
/**
* Whether or not the name of the participant which sent the message should
* be displayed.
*/
showDisplayName: boolean,
/**
* Whether or not the time at which the message was sent should be
* displayed.
*/
showTimestamp: boolean,
/**
* Invoked to receive translated strings.
*/
@@ -52,17 +28,7 @@ export type Props = {
/**
* Abstract component to display a chat message.
*/
export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
/**
* Returns the timestamp to display for the message.
*
* @returns {string}
*/
_getFormattedTimestamp() {
return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
.format(TIMESTAMP_FORMAT);
}
}
export default class AbstractChatMessage<P: Props> extends PureComponent<P> {}
/**
* Maps part of the Redux state to the props of this component.

View File

@@ -1,53 +0,0 @@
// @flow
import { PureComponent } from 'react';
export type Props = {
/**
* The messages array to render.
*/
messages: Array<Object>
}
/**
* Abstract component to display a list of chat messages, grouped by sender.
*
* @extends PureComponent
*/
export default class AbstractMessageContainer extends PureComponent<Props> {
static defaultProps = {
messages: []
};
/**
* Iterates over all the messages and creates nested arrays which hold
* consecutive messages sent by the same participant.
*
* @private
* @returns {Array<Array<Object>>}
*/
_getMessagesGroupedBySender() {
const messagesCount = this.props.messages.length;
const groups = [];
let currentGrouping = [];
let currentGroupParticipantId;
for (let i = 0; i < messagesCount; i++) {
const message = this.props.messages[i];
if (message.id === currentGroupParticipantId) {
currentGrouping.push(message);
} else {
currentGrouping.length && groups.push(currentGrouping);
currentGrouping = [ message ];
currentGroupParticipantId = message.id;
}
}
groups.push(currentGrouping);
return groups;
}
}

View File

@@ -5,7 +5,12 @@ import { KeyboardAvoidingView, SafeAreaView } from 'react-native';
import { translate } from '../../../base/i18n';
import { HeaderWithNavigation, SlidingView } from '../../../base/react';
import {
BackButton,
Header,
HeaderLabel,
SlidingView
} from '../../../base/react';
import { connect } from '../../../base/redux';
import AbstractChat, {
@@ -36,9 +41,10 @@ class Chat extends AbstractChat<Props> {
<KeyboardAvoidingView
behavior = 'padding'
style = { styles.chatContainer }>
<HeaderWithNavigation
headerLabelKey = 'chat.title'
onPressBack = { this.props._onToggleChat } />
<Header>
<BackButton onPress = { this.props._onToggleChat } />
<HeaderLabel labelKey = 'chat.title' />
</Header>
<SafeAreaView style = { styles.backdrop }>
<MessageContainer messages = { this.props._messages } />
<ChatInputBar onSend = { this.props._onSendMessage } />

View File

@@ -3,7 +3,7 @@
import React from 'react';
import { Text, View } from 'react-native';
import { translate } from '../../../base/i18n';
import { getLocalizedDateFormatter, translate } from '../../../base/i18n';
import { Avatar } from '../../../base/participants';
import { connect } from '../../../base/redux';
@@ -13,6 +13,16 @@ import AbstractChatMessage, {
} from '../AbstractChatMessage';
import styles from './styles';
/**
* Size of the rendered avatar in the message.
*/
const AVATAR_SIZE = 32;
/**
* Formatter string to display the message timestamp.
*/
const TIMESTAMP_FORMAT = 'H:mm';
/**
* Renders a single chat message.
*/
@@ -24,6 +34,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
*/
render() {
const { message } = this.props;
const timeStamp = getLocalizedDateFormatter(
new Date(message.timestamp)).format(TIMESTAMP_FORMAT);
const localMessage = message.messageType === 'local';
// Style arrays that need to be updated in various scenarios, such as
@@ -48,12 +60,18 @@ class ChatMessage extends AbstractChatMessage<Props> {
return (
<View style = { styles.messageWrapper } >
{ this._renderAvatar() }
{
// Avatar is only rendered for remote messages.
!localMessage && this._renderAvatar()
}
<View style = { detailsWrapperStyle }>
<View style = { textWrapperStyle } >
{
this.props.showDisplayName
&& this._renderDisplayName()
// Display name is only rendered for remote
// messages.
!localMessage && this._renderDisplayName()
}
<Text style = { styles.messageText }>
{ message.messageType === 'error'
@@ -64,26 +82,27 @@ class ChatMessage extends AbstractChatMessage<Props> {
: message.message }
</Text>
</View>
{ this.props.showTimestamp && this._renderTimestamp() }
<Text style = { styles.timeText }>
{ timeStamp }
</Text>
</View>
</View>
);
}
_getFormattedTimestamp: () => string;
/**
* Renders the avatar of the sender.
*
* @returns {React$Element<*>}
*/
_renderAvatar() {
const { _avatarURL } = this.props;
return (
<View style = { styles.avatarWrapper }>
{ this.props.showAvatar && <Avatar
size = { styles.avatarWrapper.width }
uri = { this.props._avatarURL } />
}
<Avatar
size = { AVATAR_SIZE }
uri = { _avatarURL } />
</View>
);
}
@@ -94,22 +113,11 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @returns {React$Element<*>}
*/
_renderDisplayName() {
const { message } = this.props;
return (
<Text style = { styles.displayName }>
{ this.props.message.displayName }
</Text>
);
}
/**
* Renders the time at which the message was sent.
*
* @returns {React$Element<*>}
*/
_renderTimestamp() {
return (
<Text style = { styles.timeText }>
{ this._getFormattedTimestamp() }
{ message.displayName }
</Text>
);
}

View File

@@ -1,86 +0,0 @@
// @flow
import React, { Component } from 'react';
import { FlatList } from 'react-native';
import ChatMessage from './ChatMessage';
import styles from './styles';
type Props = {
/**
* The messages array to render.
*/
messages: Array<Object>
}
/**
* Implements a container to render all the chat messages in a conference.
*/
export default class ChatMessageGroup extends Component<Props> {
/**
* Instantiates a new instance of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._keyExtractor = this._keyExtractor.bind(this);
this._renderMessage = this._renderMessage.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
return (
<FlatList
data = { this.props.messages }
inverted = { true }
keyExtractor = { this._keyExtractor }
renderItem = { this._renderMessage }
style = { styles.messageContainer } />
);
}
_keyExtractor: Object => string
/**
* Key extractor for the flatlist.
*
* @param {Object} item - The flatlist item that we need the key to be
* generated for.
* @param {number} index - The index of the element.
* @returns {string}
*/
_keyExtractor(item, index) {
return `key_${index}`;
}
_renderMessage: Object => React$Element<*>;
/**
* Renders a single chat message.
*
* @param {Object} message - The chat message to render.
* @returns {React$Element<*>}
*/
_renderMessage({ index, item: message }) {
return (
<ChatMessage
message = { message }
showAvatar = {
this.props.messages[0].messageType !== 'local'
&& index === this.props.messages.length - 1
}
showDisplayName = {
this.props.messages[0].messageType === 'remote'
&& index === this.props.messages.length - 1
}
showTimestamp = { index === 0 } />
);
}
}

View File

@@ -1,18 +1,23 @@
// @flow
import React from 'react';
import React, { Component } from 'react';
import { FlatList } from 'react-native';
import AbstractMessageContainer, { type Props }
from '../AbstractMessageContainer';
import ChatMessageGroup from './ChatMessageGroup';
import ChatMessage from './ChatMessage';
import styles from './styles';
type Props = {
/**
* The messages array to render.
*/
messages: Array<Object>
}
/**
* Implements a container to render all the chat messages in a conference.
*/
export default class MessageContainer extends AbstractMessageContainer {
export default class MessageContainer extends Component<Props> {
/**
* Instantiates a new instance of the component.
*
@@ -22,7 +27,7 @@ export default class MessageContainer extends AbstractMessageContainer {
super(props);
this._keyExtractor = this._keyExtractor.bind(this);
this._renderMessageGroup = this._renderMessageGroup.bind(this);
this._renderMessage = this._renderMessage.bind(this);
}
/**
@@ -33,16 +38,14 @@ export default class MessageContainer extends AbstractMessageContainer {
render() {
return (
<FlatList
data = { this._getMessagesGroupedBySender() }
data = { this.props.messages }
inverted = { true }
keyExtractor = { this._keyExtractor }
renderItem = { this._renderMessageGroup }
renderItem = { this._renderMessage }
style = { styles.messageContainer } />
);
}
_getMessagesGroupedBySender: () => Array<Array<Object>>;
_keyExtractor: Object => string
/**
@@ -57,15 +60,17 @@ export default class MessageContainer extends AbstractMessageContainer {
return `key_${index}`;
}
_renderMessageGroup: Object => React$Element<*>;
_renderMessage: Object => React$Element<*>;
/**
* Renders a single chat message.
*
* @param {Array<Object>} messages - The chat message to render.
* @param {Object} message - The chat message to render.
* @returns {React$Element<*>}
*/
_renderMessageGroup({ item: messages }) {
return <ChatMessageGroup messages = { messages } />;
_renderMessage({ item: message }) {
return (
<ChatMessage message = { message } />
);
}
}

View File

@@ -16,8 +16,7 @@ export default {
* Wrapper View for the avatar.
*/
avatarWrapper: {
marginRight: 8,
width: 32
marginRight: 8
},
/**

View File

@@ -12,8 +12,8 @@ import AbstractChat, {
type Props
} from '../AbstractChat';
import ChatInput from './ChatInput';
import ChatMessage from './ChatMessage';
import DisplayNameForm from './DisplayNameForm';
import MessageContainer from './MessageContainer';
/**
* React Component for holding the chat feature in a side panel that slides in
@@ -28,10 +28,10 @@ class Chat extends AbstractChat<Props> {
_isExited: boolean;
/**
* Reference to the React Component for displaying chat messages. Used for
* scrolling to the end of the chat messages.
* Reference to the HTML element at the end of the list of displayed chat
* messages. Used for scrolling to the end of the chat messages.
*/
_messageContainerRef: Object;
_messagesListEnd: ?HTMLElement;
/**
* Initializes a new {@code Chat} instance.
@@ -43,34 +43,32 @@ class Chat extends AbstractChat<Props> {
super(props);
this._isExited = true;
this._messageContainerRef = React.createRef();
this._messagesListEnd = null;
// Bind event handlers so they are only bound once for every instance.
this._renderMessage = this._renderMessage.bind(this);
this._renderPanelContent = this._renderPanelContent.bind(this);
// Bind event handlers so they are only bound once for every instance.
this._onChatInputResize = this._onChatInputResize.bind(this);
this._setMessageListEndRef = this._setMessageListEndRef.bind(this);
}
/**
* Implements {@code Component#componentDidMount}.
* Implements React's {@link Component#componentDidMount()}.
*
* @inheritdoc
*/
componentDidMount() {
this._scrollMessageContainerToBottom(true);
this._scrollMessagesToBottom();
}
/**
* Implements {@code Component#componentDidUpdate}.
* Updates chat input focus.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (this.props._messages !== prevProps._messages) {
this._scrollMessageContainerToBottom(true);
} else if (this.props._isOpen && !prevProps._isOpen) {
this._scrollMessageContainerToBottom(false);
this._scrollMessagesToBottom();
}
}
@@ -90,19 +88,6 @@ class Chat extends AbstractChat<Props> {
);
}
_onChatInputResize: () => void;
/**
* Callback invoked when {@code ChatInput} changes height. Preserves
* displaying the latest message if it is scrolled to.
*
* @private
* @returns {void}
*/
_onChatInputResize() {
this._messageContainerRef.current.maybeUpdateBottomScroll();
}
/**
* Returns a React Element for showing chat messages and a form to send new
* chat messages.
@@ -111,30 +96,38 @@ class Chat extends AbstractChat<Props> {
* @returns {ReactElement}
*/
_renderChat() {
const messages = this.props._messages.map(this._renderMessage);
messages.push(<div
key = 'end-marker'
ref = { this._setMessageListEndRef } />);
return (
<>
<MessageContainer
messages = { this.props._messages }
ref = { this._messageContainerRef } />
<ChatInput onResize = { this._onChatInputResize } />
</>
<div
className = 'sideToolbarContainer__inner'
id = 'chat_container'>
<div id = 'chatconversation'>
{ messages }
</div>
<ChatInput />
</div>
);
}
_renderMessage: (Object) => void;
/**
* Instantiates a React Element to display at the top of {@code Chat} to
* close {@code Chat}.
* Called by {@code _onSubmitMessage} to create the chat div.
*
* @private
* @returns {ReactElement}
* @param {string} message - The chat message to display.
* @param {string} id - The chat message ID to use as a unique key.
* @returns {Array<ReactElement>}
*/
_renderChatHeader() {
_renderMessage(message: Object, id: string) {
return (
<div className = 'chat-header'>
<div
className = 'chat-close'
onClick = { this.props._onToggleChat }>X</div>
</div>
<ChatMessage
key = { id }
message = { message } />
);
}
@@ -152,15 +145,17 @@ class Chat extends AbstractChat<Props> {
_renderPanelContent(state) {
this._isExited = state === 'exited';
const { _isOpen, _showNamePrompt } = this.props;
const { _isOpen, _onToggleChat, _showNamePrompt } = this.props;
const ComponentToRender = !_isOpen && state === 'exited'
? null
: (
<>
{ this._renderChatHeader() }
<div>
<div
className = 'chat-close'
onClick = { _onToggleChat }>X</div>
{ _showNamePrompt
? <DisplayNameForm /> : this._renderChat() }
</>
</div>
);
let className = '';
@@ -172,7 +167,7 @@ class Chat extends AbstractChat<Props> {
return (
<div
className = { `sideToolbarContainer ${className}` }
className = { className }
id = 'sideToolbarContainer'>
{ ComponentToRender }
</div>
@@ -180,18 +175,31 @@ class Chat extends AbstractChat<Props> {
}
/**
* Scrolls the chat messages so the latest message is visible.
* Automatically scrolls the displayed chat messages down to the latest.
*
* @param {boolean} withAnimation - Whether or not to show a scrolling
* animation.
* @private
* @returns {void}
*/
_scrollMessageContainerToBottom(withAnimation) {
if (this._messageContainerRef.current) {
this._messageContainerRef.current.scrollToBottom(withAnimation);
_scrollMessagesToBottom() {
if (this._messagesListEnd) {
this._messagesListEnd.scrollIntoView({
behavior: this._isExited ? 'auto' : 'smooth'
});
}
}
_setMessageListEndRef: (?HTMLElement) => void;
/**
* Sets a reference to the HTML element at the bottom of the message list.
*
* @param {Object} messageListEnd - The HTML element.
* @private
* @returns {void}
*/
_setMessageListEndRef(messageListEnd: ?HTMLElement) {
this._messagesListEnd = messageListEnd;
}
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));

View File

@@ -2,10 +2,8 @@
import React, { Component } from 'react';
import Emoji from 'react-emoji-render';
import TextareaAutosize from 'react-textarea-autosize';
import type { Dispatch } from 'redux';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { sendMessage } from '../../actions';
@@ -23,15 +21,9 @@ type Props = {
dispatch: Dispatch<any>,
/**
* Optional callback to invoke when the chat textarea has auto-resized to
* fit overflowing text.
* Optional callback to get a reference to the chat input element.
*/
onResize: ?Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
getChatInputRef?: Function
};
/**
@@ -92,7 +84,7 @@ class ChatInput extends Component<Props, State> {
* HTML Textareas do not support autofocus. Simulate autofocus by
* manually focusing.
*/
this._focus();
this.focus();
}
/**
@@ -121,14 +113,13 @@ class ChatInput extends Component<Props, State> {
</div>
</div>
<div className = 'usrmsg-form'>
<TextareaAutosize
<textarea
data-i18n = '[placeholder]chat.messagebox'
id = 'usermsg'
inputRef = { this._setTextAreaRef }
maxRows = { 5 }
onChange = { this._onMessageChange }
onHeightChange = { this.props.onResize }
onKeyDown = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') }
placeholder = { 'Enter Text...' }
ref = { this._setTextAreaRef }
value = { this.state.message } />
</div>
</div>
@@ -136,12 +127,20 @@ class ChatInput extends Component<Props, State> {
}
/**
* Place cursor focus on this component's text area.
* Removes cursor focus on this component's text area.
*
* @private
* @returns {void}
*/
_focus() {
blur() {
this._textArea && this._textArea.blur();
}
/**
* Place cursor focus on this component's text area.
*
* @returns {void}
*/
focus() {
this._textArea && this._textArea.focus();
}
@@ -199,7 +198,7 @@ class ChatInput extends Component<Props, State> {
showSmileysPanel: false
});
this._focus();
this.focus();
}
_onToggleSmileysPanel: () => void;
@@ -213,7 +212,7 @@ class ChatInput extends Component<Props, State> {
_onToggleSmileysPanel() {
this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
this._focus();
this.focus();
}
_setTextAreaRef: (?HTMLTextAreaElement) => void;
@@ -227,7 +226,11 @@ class ChatInput extends Component<Props, State> {
*/
_setTextAreaRef(textAreaElement: ?HTMLTextAreaElement) {
this._textArea = textAreaElement;
if (this.props.getChatInputRef) {
this.props.getChatInputRef(textAreaElement);
}
}
}
export default translate(connect()(ChatInput));
export default connect()(ChatInput);

View File

@@ -23,12 +23,24 @@ class ChatMessage extends AbstractChatMessage<Props> {
*/
render() {
const { message } = this.props;
const messageToDisplay = message.messageType === 'error'
? this.props.t('chat.error', {
let messageTypeClassname = '';
let messageToDisplay = message.message;
switch (message.messageType) {
case 'local':
messageTypeClassname = 'localuser';
break;
case 'error':
messageTypeClassname = 'error';
messageToDisplay = this.props.t('chat.error', {
error: message.error,
originalText: message.message
})
: message.message;
originalText: messageToDisplay
});
break;
default:
messageTypeClassname = 'remoteuser';
}
// replace links and smileys
// Strophe already escapes special symbols on sending,
@@ -56,44 +68,47 @@ class ChatMessage extends AbstractChatMessage<Props> {
});
return (
<div className = 'chatmessage-wrapper'>
<div className = 'chatmessage'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
{ processedMessage }
</div>
<div className = { `chatmessage ${messageTypeClassname}` }>
<div className = 'chatArrow' />
<div className = 'display-name'>
{ message.displayName }
</div>
<div className = { 'timestamp' }>
{ ChatMessage.formatTimestamp(message.timestamp) }
</div>
<div className = 'usermessage'>
{ processedMessage }
</div>
{ this.props.showTimestamp && this._renderTimestamp() }
</div>
);
}
_getFormattedTimestamp: () => string;
/**
* Renders the display name of the sender.
*
* @returns {React$Element<*>}
*/
_renderDisplayName() {
return (
<div className = 'display-name'>
{ this.props.message.displayName }
</div>
);
}
/**
* Renders the time at which the message was sent.
* Returns a timestamp formatted for display.
*
* @returns {React$Element<*>}
* @param {number} timestamp - The timestamp for the chat message.
* @private
* @returns {string}
*/
_renderTimestamp() {
return (
<div className = 'timestamp'>
{ this._getFormattedTimestamp() }
</div>
);
static formatTimestamp(timestamp) {
const now = new Date(timestamp);
let hour = now.getHours();
let minute = now.getMinutes();
let second = now.getSeconds();
if (hour.toString().length === 1) {
hour = `0${hour}`;
}
if (minute.toString().length === 1) {
minute = `0${minute}`;
}
if (second.toString().length === 1) {
second = `0${second}`;
}
return `${hour}:${minute}:${second}`;
}
}

View File

@@ -1,60 +0,0 @@
// @flow
import React, { Component } from 'react';
import ChatMessage from './ChatMessage';
type Props = {
/**
* Additional CSS classes to apply to the root element.
*/
className: string,
/**
* The messages to display as a group.
*/
messages: Array<Object>,
};
/**
* Displays a list of chat messages. Will show only the display name for the
* first chat message and the timestamp for the last chat message.
*
* @extends React.Component
*/
class ChatMessageGroup extends Component<Props> {
static defaultProps = {
className: ''
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const { className, messages } = this.props;
const messagesLength = messages.length;
if (!messagesLength) {
return null;
}
return (
<div className = { `chat-message-group ${className}` }>
{
messages.map((message, i) => (
<ChatMessage
key = { i }
message = { message }
showDisplayName = { i === 0 }
showTimestamp = { i === messages.length - 1 } />
))
}
</div>
);
}
}
export default ChatMessageGroup;

View File

@@ -1,123 +0,0 @@
// @flow
import React from 'react';
import AbstractMessageContainer, { type Props }
from '../AbstractMessageContainer';
import ChatMessageGroup from './ChatMessageGroup';
/**
* Displays all received chat messages, grouped by sender.
*
* @extends AbstractMessageContainer
*/
export default class MessageContainer extends AbstractMessageContainer {
/**
* Whether or not chat has been scrolled to the bottom of the screen. Used
* to determine if chat should be scrolled automatically to the bottom when
* the {@code ChatInput} resizes.
*/
_isScrolledToBottom: boolean;
/**
* Reference to the HTML element at the end of the list of displayed chat
* messages. Used for scrolling to the end of the chat messages.
*/
_messagesListEndRef: Object;
/**
* A React ref to the HTML element containing all {@code ChatMessageGroup}
* instances.
*/
_messageListRef: Object;
/**
* Initializes a new {@code MessageContainer} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code MessageContainer} instance with.
*/
constructor(props: Props) {
super(props);
this._isScrolledToBottom = true;
this._messageListRef = React.createRef();
this._messagesListEndRef = React.createRef();
this._onChatScroll = this._onChatScroll.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const groupedMessages = this._getMessagesGroupedBySender();
const messages = groupedMessages.map((group, index) => {
const messageType = group[0] && group[0].messageType;
return (
<ChatMessageGroup
className = { messageType || 'remote' }
key = { index }
messages = { group } />
);
});
return (
<div
id = 'chatconversation'
onScroll = { this._onChatScroll }
ref = { this._messageListRef }>
{ messages }
<div ref = { this._messagesListEndRef } />
</div>
);
}
/**
* Scrolls to the bottom again if the instance had previously been scrolled
* to the bottom. This method is used when a resize has occurred below the
* instance and bottom scroll needs to be maintained.
*
* @returns {void}
*/
maybeUpdateBottomScroll() {
if (this._isScrolledToBottom) {
this.scrollToBottom(false);
}
}
/**
* Automatically scrolls the displayed chat messages down to the latest.
*
* @param {boolean} withAnimation - Whether or not to show a scrolling
* animation.
* @returns {void}
*/
scrollToBottom(withAnimation: boolean) {
this._messagesListEndRef.current.scrollIntoView({
behavior: withAnimation ? 'smooth' : 'auto'
});
}
_getMessagesGroupedBySender: () => Array<Array<Object>>;
_onChatScroll: () => void;
/**
* Callback invoked to listen to the current scroll location.
*
* @private
* @returns {void}
*/
_onChatScroll() {
const element = this._messageListRef.current;
this._isScrolledToBottom
= element.scrollHeight - element.scrollTop === element.clientHeight;
}
}

View File

@@ -1,13 +1,13 @@
// @flow
import React from 'react';
import { BackHandler, NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
import { BackHandler, SafeAreaView, StatusBar, View } from 'react-native';
import { appNavigate } from '../../../app';
import { getAppProp } from '../../../base/app';
import { getParticipantCount } from '../../../base/participants';
import { Container, LoadingIndicator, TintedView } from '../../../base/react';
import { connect } from '../../../base/redux';
import { connect as reactReduxConnect } from '../../../base/redux';
import {
isNarrowAspectRatio,
makeAspectRatioAware
@@ -64,6 +64,25 @@ type Props = AbstractProps & {
*/
_largeVideoParticipantId: string,
/**
* The handler which dispatches the (redux) action disconnect.
*
* @private
* @returns {void}
*/
_onDisconnect: Function,
/**
* Handles a hardware button press for back navigation. Leaves the
* associated {@code Conference}.
*
* @private
* @returns {boolean} As the associated conference is unconditionally left
* and exiting the app while it renders a {@code Conference} is undesired,
* {@code true} is always returned.
*/
_onHardwareBackPress: Function,
/**
* The number of participants in the conference.
*
@@ -71,13 +90,6 @@ type Props = AbstractProps & {
*/
_participantCount: number,
/**
* Whether Picture-in-Picture is enabled.
*
* @private
*/
_pictureInPictureEnabled: boolean,
/**
* The indicator which determines whether the UI is reduced (to accommodate
* smaller display areas).
@@ -109,12 +121,7 @@ type Props = AbstractProps & {
*
* @private
*/
_toolboxAlwaysVisible: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
_toolboxAlwaysVisible: boolean
};
/**
@@ -132,8 +139,6 @@ class Conference extends AbstractConference<Props, *> {
// Bind event handlers so they are only bound once per instance.
this._onClick = this._onClick.bind(this);
this._onHardwareBackPress = this._onHardwareBackPress.bind(this);
this._setToolboxVisible = this._setToolboxVisible.bind(this);
}
/**
@@ -144,11 +149,15 @@ class Conference extends AbstractConference<Props, *> {
* @returns {void}
*/
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
BackHandler.addEventListener(
'hardwareBackPress',
this.props._onHardwareBackPress);
// Show the toolbox if we are the only participant; otherwise, the whole
// UI looks too unpopulated the LargeVideo visible.
this.props._participantCount === 1 && this._setToolboxVisible(true);
const { _participantCount, _setToolboxVisible } = this.props;
_participantCount === 1 && _setToolboxVisible(true);
}
/**
@@ -156,23 +165,24 @@ class Conference extends AbstractConference<Props, *> {
*
* @inheritdoc
*/
componentDidUpdate(prevProps: Props) {
componentDidUpdate(pevProps: Props) {
const {
_participantCount: oldParticipantCount
} = prevProps;
} = pevProps;
const {
_participantCount: newParticipantCount,
_setToolboxVisible,
_toolboxVisible
} = this.props;
if (oldParticipantCount === 1
&& newParticipantCount > 1
&& _toolboxVisible) {
this._setToolboxVisible(false);
_setToolboxVisible(false);
} else if (oldParticipantCount > 1
&& newParticipantCount === 1
&& !_toolboxVisible) {
this._setToolboxVisible(true);
_setToolboxVisible(true);
}
}
@@ -186,7 +196,9 @@ class Conference extends AbstractConference<Props, *> {
*/
componentWillUnmount() {
// Tear handling any hardware button presses for back navigation down.
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
BackHandler.removeEventListener(
'hardwareBackPress',
this.props._onHardwareBackPress);
}
/**
@@ -295,33 +307,9 @@ class Conference extends AbstractConference<Props, *> {
return;
}
this._setToolboxVisible(!this.props._toolboxVisible);
}
const toolboxVisible = !this.props._toolboxVisible;
_onHardwareBackPress: () => boolean;
/**
* Handles a hardware button press for back navigation. Enters Picture-in-Picture mode
* (if supported) or leaves the associated {@code Conference} otherwise.
*
* @returns {boolean} Exiting the app is undesired, so {@code true} is always returned.
*/
_onHardwareBackPress() {
let p;
if (this.props._pictureInPictureEnabled) {
const { PictureInPicture } = NativeModules;
p = PictureInPicture.enterPictureInPicture();
} else {
p = Promise.reject(new Error('PiP not enabled'));
}
p.catch(() => {
this.props.dispatch(appNavigate(undefined));
});
return true;
this.props._setToolboxVisible(toolboxVisible);
}
/**
@@ -369,20 +357,46 @@ class Conference extends AbstractConference<Props, *> {
}
);
}
}
_setToolboxVisible: (boolean) => void;
/**
* Maps dispatching of some action to React component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @private
* @returns {{
* _onHardwareBackPress: Function,
* _setToolboxVisible: Function
* }}
*/
function _mapDispatchToProps(dispatch) {
return {
/**
* Handles a hardware button press for back navigation. Leaves the
* associated {@code Conference}.
*
* @returns {boolean} As the associated conference is unconditionally
* left and exiting the app while it renders a {@code Conference} is
* undesired, {@code true} is always returned.
*/
_onHardwareBackPress() {
dispatch(appNavigate(undefined));
/**
* Dispatches an action changing the visibility of the {@link Toolbox}.
*
* @private
* @param {boolean} visible - Pass {@code true} to show the
* {@code Toolbox} or {@code false} to hide it.
* @returns {void}
*/
_setToolboxVisible(visible) {
this.props.dispatch(setToolboxVisible(visible));
}
return true;
},
/**
* Dispatches an action changing the visibility of the {@link Toolbox}.
*
* @private
* @param {boolean} visible - Pass {@code true} to show the
* {@code Toolbox} or {@code false} to hide it.
* @returns {void}
*/
_setToolboxVisible(visible) {
dispatch(setToolboxVisible(visible));
}
};
}
/**
@@ -446,14 +460,6 @@ function _mapStateToProps(state) {
*/
_participantCount: getParticipantCount(state),
/**
* Whether Picture-in-Picture is enabled.
*
* @private
* @type {boolean}
*/
_pictureInPictureEnabled: getAppProp(state, 'pictureInPictureEnabled'),
/**
* The indicator which determines whether the UI is reduced (to
* accommodate smaller display areas).
@@ -481,4 +487,5 @@ function _mapStateToProps(state) {
};
}
export default connect(_mapStateToProps)(makeAspectRatioAware(Conference));
export default reactReduxConnect(_mapStateToProps, _mapDispatchToProps)(
makeAspectRatioAware(Conference));

View File

@@ -6,7 +6,6 @@ import {
import { createDeviceChangedEvent, sendAnalytics } from '../analytics';
import {
getDeviceLabelById,
setAudioInputDevice,
setAudioOutputDeviceId,
setVideoInputDevice
@@ -113,9 +112,7 @@ export function submitDeviceSelectionTab(newState) {
&& newState.selectedVideoInputId
!== currentState.selectedVideoInputId) {
dispatch(updateSettings({
userSelectedCameraDeviceId: newState.selectedVideoInputId,
userSelectedCameraDeviceLabel:
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
userSelectedCameraDeviceId: newState.selectedVideoInputId
}));
dispatch(
@@ -126,9 +123,7 @@ export function submitDeviceSelectionTab(newState) {
&& newState.selectedAudioInputId
!== currentState.selectedAudioInputId) {
dispatch(updateSettings({
userSelectedMicDeviceId: newState.selectedAudioInputId,
userSelectedMicDeviceLabel:
getDeviceLabelById(getState(), newState.selectedAudioInputId, 'audioInput')
userSelectedMicDeviceId: newState.selectedAudioInputId
}));
dispatch(
@@ -143,8 +138,7 @@ export function submitDeviceSelectionTab(newState) {
setAudioOutputDeviceId(
newState.selectedAudioOutputId,
dispatch,
true,
getDeviceLabelById(getState(), newState.selectedAudioOutputId, 'audioOutput'))
true)
.then(() => logger.log('changed audio output device'))
.catch(err => {
logger.warn(

View File

@@ -113,7 +113,6 @@ class AudioOutputPreview extends Component<Props> {
*/
_setAudioSink() {
this._audioElement
&& this.props.deviceId
&& this._audioElement.setSinkId(this.props.deviceId);
}
}

View File

@@ -15,11 +15,6 @@ import {
} from '../base/devices';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { toState } from '../base/redux';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId,
getUserSelectedOutputDeviceId
} from '../base/settings';
/**
* Returns the properties for the device selection dialog from Redux state.
@@ -43,9 +38,9 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
// on welcome page we also show only what we have saved as user selected devices
if (!conference) {
disableAudioInputChange = false;
selectedAudioInputId = getUserSelectedMicDeviceId(state);
selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
selectedVideoInputId = getUserSelectedCameraDeviceId(state);
selectedAudioInputId = settings.userSelectedMicDeviceId;
selectedAudioOutputId = settings.userSelectedAudioOutputDeviceId;
selectedVideoInputId = settings.userSelectedCameraDeviceId;
}
// we fill the device selection dialog with the devices that are currently

View File

@@ -10,7 +10,6 @@ import { Audio, MEDIA_TYPE } from '../../../base/media';
import {
PARTICIPANT_ROLE,
ParticipantView,
isEveryoneModerator,
isLocalParticipantModerator,
pinParticipant
} from '../../../base/participants';
@@ -39,11 +38,6 @@ type Props = {
*/
_audioTrack: Object,
/**
* True if everone in the meeting is moderator.
*/
_isEveryoneModerator: boolean,
/**
* True if the local participant is a moderator.
*/
@@ -123,7 +117,6 @@ class Thumbnail extends Component<Props> {
render() {
const {
_audioTrack: audioTrack,
_isEveryoneModerator,
_isModerator,
_largeVideo: largeVideo,
_onClick,
@@ -179,7 +172,7 @@ class Thumbnail extends Component<Props> {
{ renderDisplayName && <DisplayNameLabel participantId = { participantId } /> }
{ !_isEveryoneModerator && participant.role === PARTICIPANT_ROLE.MODERATOR
{ participant.role === PARTICIPANT_ROLE.MODERATOR
&& <View style = { styles.moderatorIndicatorContainer }>
<ModeratorIndicator />
</View> }
@@ -282,7 +275,6 @@ function _mapStateToProps(state, ownProps) {
return {
_audioTrack: audioTrack,
_isEveryoneModerator: isEveryoneModerator(state),
_isModerator: isLocalParticipantModerator(state),
_largeVideo: largeVideo,
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),

View File

@@ -41,16 +41,6 @@ export const REMOVE_PENDING_INVITE_REQUESTS
*/
export const SET_CALLEE_INFO_VISIBLE = 'SET_CALLEE_INFO_VISIBLE';
/**
* The type of Redux action to set the visibility of the dial in summary.
*
* {
* type: SET_DIAL_IN_SUMMARY_VISIBLE,
* visible: boolean
* }
*/
export const SET_DIAL_IN_SUMMARY_VISIBLE = 'SET_DIAL_IN_SUMMARY_VISIBLE';
/**
* The type of redux action which sets the invite dialog visible or invisible.
*

View File

@@ -11,7 +11,6 @@ import {
BEGIN_ADD_PEOPLE,
REMOVE_PENDING_INVITE_REQUESTS,
SET_CALLEE_INFO_VISIBLE,
SET_DIAL_IN_SUMMARY_VISIBLE,
SET_INVITE_DIALOG_VISIBLE,
UPDATE_DIAL_IN_NUMBERS_FAILED,
UPDATE_DIAL_IN_NUMBERS_SUCCESS
@@ -257,15 +256,6 @@ export function addPendingInviteRequest(
};
}
/**
* Action to hide the dial in summary.
*
* @returns {showDialInSummary}
*/
export function hideDialInSummary() {
return showDialInSummary(undefined);
}
/**
* Removes all pending invite requests.
*
@@ -278,19 +268,3 @@ export function removePendingInviteRequests() {
type: REMOVE_PENDING_INVITE_REQUESTS
};
}
/**
* Action to set the dial in summary url (and show it).
*
* @param {?string} locationUrl - The location URL to show the dial in summary for.
* @returns {{
* type: SET_DIAL_IN_SUMMARY_VISIBLE,
* summaryUrl: ?string
* }}
*/
export function showDialInSummary(locationUrl: ?string) {
return {
type: SET_DIAL_IN_SUMMARY_VISIBLE,
summaryUrl: locationUrl
};
}

View File

@@ -16,7 +16,10 @@ import { Icon } from '../../../../base/font-icons';
import { translate } from '../../../../base/i18n';
import {
AvatarListItem,
HeaderWithNavigation,
BackButton,
ForwardButton,
Header,
HeaderLabel,
Modal,
type Item
} from '../../../../base/react';
@@ -143,12 +146,14 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
<Modal
onRequestClose = { this._onCloseAddPeopleDialog }
visible = { this.props._isVisible }>
<HeaderWithNavigation
forwardDisabled = { this._isAddDisabled() }
forwardLabelKey = 'inviteDialog.send'
headerLabelKey = 'inviteDialog.header'
onPressBack = { this._onCloseAddPeopleDialog }
onPressForward = { this._onInvite } />
<Header>
<BackButton onPress = { this._onCloseAddPeopleDialog } />
<HeaderLabel labelKey = 'inviteDialog.header' />
<ForwardButton
disabled = { this._isAddDisabled() }
labelKey = 'inviteDialog.send'
onPress = { this._onInvite } />
</Header>
<SafeAreaView style = { styles.dialogWrapper }>
<View
style = { styles.searchFieldWrapper }>

View File

@@ -55,7 +55,6 @@ export default {
backgroundColor: 'rgb(240, 243, 247)',
borderBottomRightRadius: 10,
borderTopRightRadius: 10,
color: DARK_GREY,
flex: 1,
fontSize: 17,
paddingVertical: 7

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