Compare commits

..

54 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
fbdd1d2f38 settings: fix loading disableCallIntegration 2019-11-08 12:22:41 +01:00
Saúl Ibarra Corretgé
53ebab33b3 android: fix selecting the Bluetooth route
Samsung devices (of course) seem to stick with the earpiece if we first select
Bluetooth but then set speaker to false. Reverse the order to make everyone
happy.

This only applies to the generic and legacy handlers.
2019-11-08 12:22:41 +01:00
Saúl Ibarra Corretgé
a36965e908 android: fix initializing audio device handler modules too early
When ConnectionService is used (the default) we were attaching the handlers too
early, and since attaching them requires that the RNConnectionService module is
loaded, it silently failed. Instead, use the initialize() method, which gets
called after all the Catalyst (aka native) modules have been loaded.
2019-11-08 12:22:41 +01:00
Saúl Ibarra Corretgé
9bf859ee50 android: log a warning if listeners could not be attached 2019-11-08 12:22:41 +01:00
Saúl Ibarra Corretgé
24ff6f58a5 android: make code a bit more readable 2019-11-08 12:22:41 +01:00
Saúl Ibarra Corretgé
6338624095 android,hack: send custom UA to fool spartan nginx config on meet.jit.si 2019-11-07 15:28:04 +01:00
Saúl Ibarra Corretgé
9634d58d4e Merge branch 'master' into mobile-19.4 2019-11-07 08:40:06 +01:00
Saúl Ibarra Corretgé
ad0064993d ios: enable Swift mobule stability for the SDK target
Supersedes: https://github.com/jitsi/jitsi-meet/pull/4818
Fixes: https://github.com/jitsi/jitsi-meet/issues/4812
2019-11-06 18:30:26 +01:00
Saúl Ibarra Corretgé
458d4acd22 ios: use the "new" Xcode build system
It was introduced in Xcode 9 and made the default in Xcode 10. We were forcing
the use of the legacy version, which doesn't support some new features that we
wish to enable, such as building the SDK for distribution.
2019-11-06 18:30:26 +01:00
Saúl Ibarra Corretgé
8ebc99175c ios: set deployment target on Pods to 10.0
Matches the app / SDK deployment target and avoids compilation warnings.
2019-11-06 18:30:26 +01:00
Vlad Piersec
9889cb2b69 Add conference name as fallback for subject 2019-11-06 17:23:18 +01:00
Saúl Ibarra Corretgé
95a6f1355f uri: avoid using String.prototype.normalize
It crashes on Android. Well, on the JSC version React Native uses on Android.

While we could use this fallback only on Android, we have decided to use it
on all mobile platforms for consistency.
2019-11-06 15:37:52 +01:00
Saúl Ibarra Corretgé
191e530071 uri: avoid using String.prototype.normalize
It crashes on Android. Well, on the JSC version React Native uses on Android.

While we could use this fallback only on Android, we have decided to use it
on all mobile platforms for consistency.
2019-11-06 15:37:14 +01:00
Mihai Uscat
ae30d39b4d feat(PromotionalFooter): Implement 2019-11-06 03:29:49 -08:00
Leonard Kim
c354e46846 chore(deps): update lib so newer FF does not need click for gum 2019-11-06 07:47:14 +00:00
Hristo Terezov
5da4e43e50 fix(settings): respect configWhitelist 2019-11-05 02:13:54 -08:00
Hristo Terezov
eae6f7760f fix(configWhitelist): add startWithAudioMuted. 2019-11-05 02:13:54 -08:00
Mihai Uscat
00161212c8 feat(welcome): Add responsive text to go button 2019-11-04 05:48:55 -08:00
Mark Anthony Sison
8976b92842 doc(install): adds cd command to jitsi-meet installation 2019-11-03 19:46:34 +00:00
Vlad Piersec
c3a6a8fb17 Add participants count 2019-10-31 09:08:59 -07:00
Saúl Ibarra Corretgé
fd3fb7ef55 Merge branch 'master' into mobile-19.4 2019-10-31 16:45:37 +01:00
Saúl Ibarra Corretgé
391e5ca483 deps: react-native@0.61.3 2019-10-31 16:44:31 +01:00
Saúl Ibarra Corretgé
36654cb808 rn: disable H.264 on select devices even when not in P2P
iOS 10 crashes, so don't use it there, in any case.
2019-10-31 16:41:08 +01:00
Saúl Ibarra Corretgé
6d16e087d9 rn: add a new advanced settings section
Currently only 2 options are implemented, mainly aimed at helping troubleshoot
audio related problems:

- Disable native call integration (it disables CallKit / ConnectionService)
- Disable P2P
2019-10-31 16:41:08 +01:00
Saúl Ibarra Corretgé
fe90e5aa8f rn,settings: remove top margin 2019-10-31 16:41:08 +01:00
Saúl Ibarra Corretgé
3c22cd8ef4 rn,android: refactor audio device handling module
Separate each implementation (3 as of this writing) into each own "handler"
class.

This should make the code easier to understand, maintain and extend.
2019-10-31 16:41:08 +01:00
Bettenbuk Zoltan
5429b8568e feat: feature flag for invite functionalities 2019-10-29 11:27:25 +01:00
George Politis
0eccaf9a21 bumps ljm@5521a40aa85cb6f128f8a6dad9b72a5646132484 (#4791) 2019-10-24 14:52:38 +02:00
Aaron van Meerten
be0950c1ec multidomain mapper functionality and examples (#4773)
* first pass at mod_muc_domain open source plus example

* doc - prosody config and config.js examples for mapper
2019-10-24 12:42:11 +01:00
drimovecz
6ecd150f75 Add context user on speaker stats 2019-10-23 09:24:43 +01:00
Mihai Uscat
02fb37189b fix(welcome): Add extra variables 2019-10-22 13:24:44 -07:00
Jaya Allamsetty
8fe2536996 Update LJM for taking the changes for capScreenshareBitrate 2019-10-22 13:28:14 -05:00
Paweł Domas
4b9e156c5d Generic iOS .ipa build script (#4775) 2019-10-22 12:45:28 -05:00
Bettenbuk Zoltan
9265e1ffec ui: web chat facelift 2019-10-22 13:16:00 +02:00
Bettenbuk Zoltan
d11735b04c feat: make the hangup button first 2019-10-21 19:00:12 +02:00
Saúl Ibarra Corretgé
d33b700477 rn,blank-page: refactor BlankPage
- Remove network-activity "feature"
    - It wasn't in use
    - It relied on internal React Native components, bound to break anytime
- Show an infinite loading indicator
- Style it just like the LoadConfigOverlay
    - Since it kinda represents the opposite, an "unload" then SDK is done
2019-10-21 11:17:56 +02:00
Saúl Ibarra Corretgé
6909c6a0f4 android: fix SDK release script for new dependency syntax
Skip the first character, since it's now like ^123456.0.0
2019-10-21 11:13:13 +02:00
Saúl Ibarra Corretgé
97d75c2cb9 android: fix SDK release script for new dependency syntax
Skip the first character, since it's now like ^123456.0.0
2019-10-21 11:12:26 +02:00
Saúl Ibarra Corretgé
287115f4c3 deps: react-native-webrtc@latest
Fixes a crash on iOS when disposing streams.
2019-10-18 14:05:18 +02:00
Saúl Ibarra Corretgé
63a221212b ios: add ability to manually toggle WebRTC logging 2019-10-18 14:05:18 +02:00
Bettenbuk Zoltan
6a916fd0e1 fix: fix filmstrip-only toolbar 2019-10-18 13:16:15 +02:00
yanas
220691d61d Merge pull request #4762 from jitsi/fix-etherpad-follow-me
Fixes showing etherpad in follow-me mode.
2019-10-17 13:44:10 +01:00
damencho
5cafc4bcbd Fixes showing etherpad in follow-me mode. 2019-10-17 13:39:01 +01:00
yanas
b3a78dc2e6 Merge pull request #4761 from zbettenbuk/aot-icons
fix: fix and refactor AoT css
2019-10-17 12:34:13 +01:00
Bettenbuk Zoltan
bebc6eabe5 fix: fix and refactor AoT css 2019-10-17 12:15:29 +02:00
paweldomas
26dc6a4ac2 update logger and LJM to support log timestamps 2019-10-16 15:59:58 -05:00
Hristo Terezov
ff2626723a fix(HelpButton): Improvements. 2019-10-16 13:34:43 -07:00
Mihai Uscat
72bb897269 feat(DownloadOverflowButton): Implement. 2019-10-16 11:30:06 -07:00
damencho
f46387a226 Adds room name validation logic for web. 2019-10-16 17:52:24 +01:00
damencho
a4cbbccb2a Fixes loading recent lists on wrong meeting name stored.
decodeURIComponent is not needed any more and after adding a validation such meeting name should not happen to be stored.
2019-10-16 17:52:24 +01:00
damencho
3e1a008399 Adds copy icon next to the meeting url in info dialog. 2019-10-16 17:52:24 +01:00
Bettenbuk Zoltan
7e70a8c1de feat: make mobile chat messages selectable 2019-10-16 16:05:10 +02:00
Hristo Terezov
8efee04a10 feat(package.json): Node 12 support. 2019-10-16 06:34:44 -07:00
Bettenbuk Zoltan
a35099f949 feat: add chat color scheming 2019-10-16 10:38:28 +02:00
113 changed files with 4711 additions and 3988 deletions

View File

@@ -10,7 +10,7 @@ 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)
JSC_VERSION="r"$(jq -r '.dependencies."jsc-android"' ${THIS_DIR}/../../node_modules/react-native/package.json | cut -d . -f 1)
JSC_VERSION="r"$(jq -r '.dependencies."jsc-android"' ${THIS_DIR}/../../node_modules/react-native/package.json | cut -d . -f 1 | cut -c 2-)
DO_GIT_TAG=${GIT_TAG:-0}
if [[ $THE_MVN_REPO == http* ]]; then

View File

@@ -0,0 +1,165 @@
/*
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.content.Context;
import android.os.Build;
import android.telecom.CallAudioState;
import androidx.annotation.RequiresApi;
import java.util.HashSet;
import java.util.Set;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
/**
* {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
* Android versions >= O when ConnectionService is enabled.
*/
@RequiresApi(Build.VERSION_CODES.O)
class AudioDeviceHandlerConnectionService implements
AudioModeModule.AudioDeviceHandlerInterface,
RNConnectionService.CallAudioStateListener {
private final static String TAG = AudioDeviceHandlerConnectionService.class.getSimpleName();
/**
* Reference to the main {@code AudioModeModule}.
*/
private AudioModeModule module;
/**
* Converts any of the "DEVICE_" constants into the corresponding
* {@link android.telecom.CallAudioState} "ROUTE_" number.
*
* @param audioDevice one of the "DEVICE_" constants.
* @return a route number {@link android.telecom.CallAudioState#ROUTE_EARPIECE} if
* no match is found.
*/
private static int audioDeviceToRouteInt(String audioDevice) {
if (audioDevice == null) {
return CallAudioState.ROUTE_EARPIECE;
}
switch (audioDevice) {
case AudioModeModule.DEVICE_BLUETOOTH:
return CallAudioState.ROUTE_BLUETOOTH;
case AudioModeModule.DEVICE_EARPIECE:
return CallAudioState.ROUTE_EARPIECE;
case AudioModeModule.DEVICE_HEADPHONES:
return CallAudioState.ROUTE_WIRED_HEADSET;
case AudioModeModule.DEVICE_SPEAKER:
return CallAudioState.ROUTE_SPEAKER;
default:
JitsiMeetLogger.e(TAG + " Unsupported device name: " + audioDevice);
return CallAudioState.ROUTE_EARPIECE;
}
}
/**
* Populates given route mask into the "DEVICE_" list.
*
* @param supportedRouteMask an integer coming from
* {@link android.telecom.CallAudioState#getSupportedRouteMask()}.
* @return a list of device names.
*/
private static Set<String> routesToDeviceNames(int supportedRouteMask) {
Set<String> devices = new HashSet<>();
if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE) == CallAudioState.ROUTE_EARPIECE) {
devices.add(AudioModeModule.DEVICE_EARPIECE);
}
if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH) == CallAudioState.ROUTE_BLUETOOTH) {
devices.add(AudioModeModule.DEVICE_BLUETOOTH);
}
if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER) == CallAudioState.ROUTE_SPEAKER) {
devices.add(AudioModeModule.DEVICE_SPEAKER);
}
if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET) == CallAudioState.ROUTE_WIRED_HEADSET) {
devices.add(AudioModeModule.DEVICE_HEADPHONES);
}
return devices;
}
/**
* Used to store the most recently reported audio devices.
* Makes it easier to compare for a change, because the devices are stored
* as a mask in the {@link android.telecom.CallAudioState}. The mask is populated into
* the {@code availableDevices} on each update.
*/
private int supportedRouteMask = -1;
public AudioDeviceHandlerConnectionService() {
}
@Override
public void onCallAudioStateChange(final CallAudioState state) {
module.runInAudioThread(new Runnable() {
@Override
public void run() {
boolean audioRouteChanged
= audioDeviceToRouteInt(module.getSelectedDevice()) != state.getRoute();
int newSupportedRoutes = state.getSupportedRouteMask();
boolean audioDevicesChanged = supportedRouteMask != newSupportedRoutes;
if (audioDevicesChanged) {
supportedRouteMask = newSupportedRoutes;
Set<String> devices = routesToDeviceNames(supportedRouteMask);
module.replaceDevices(devices);
JitsiMeetLogger.i(TAG + " Available audio devices: " + devices.toString());
}
if (audioRouteChanged || audioDevicesChanged) {
module.resetSelectedDevice();
module.updateAudioRoute();
}
}
});
}
@Override
public void start(Context context, AudioModeModule audioModeModule) {
JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
module = audioModeModule;
RNConnectionService rcs = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
if (rcs != null) {
rcs.setCallAudioStateListener(this);
} else {
JitsiMeetLogger.w(TAG + " Couldn't set call audio state listener, module is null");
}
}
@Override
public void stop() {
RNConnectionService rcs = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
if (rcs != null) {
rcs.setCallAudioStateListener(null);
} else {
JitsiMeetLogger.w(TAG + " Couldn't set call audio state listener, module is null");
}
}
public void setAudioRoute(String audioDevice) {
int newAudioRoute = audioDeviceToRouteInt(audioDevice);
RNConnectionService.setAudioRoute(newAudioRoute);
}
@Override
public boolean setMode(int mode) {
return true;
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import androidx.annotation.RequiresApi;
import java.util.HashSet;
import java.util.Set;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
/**
* {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
* all post-M Android versions. This handler can be used on any Android versions >= M, but by
* default it's only used on versions < O, since versions >= O use ConnectionService, but it
* can be disabled.
*/
@RequiresApi(Build.VERSION_CODES.M)
class AudioDeviceHandlerGeneric implements
AudioModeModule.AudioDeviceHandlerInterface,
AudioManager.OnAudioFocusChangeListener {
private final static String TAG = AudioDeviceHandlerGeneric.class.getSimpleName();
/**
* Reference to the main {@code AudioModeModule}.
*/
private AudioModeModule module;
/**
* Constant defining a USB headset. Only available on API level >= 26.
* The value of: AudioDeviceInfo.TYPE_USB_HEADSET
*/
private static final int TYPE_USB_HEADSET = 22;
/**
* Indicator that we have lost audio focus.
*/
private boolean audioFocusLost = false;
/**
* {@link AudioManager} instance used to interact with the Android audio
* subsystem.
*/
private AudioManager audioManager;
/**
* {@link Runnable} for running audio device detection the main thread.
* This is only used on Android >= M.
*/
private final Runnable onAudioDeviceChangeRunner = new Runnable() {
@Override
public void run() {
Set<String> devices = new HashSet<>();
AudioDeviceInfo[] deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
for (AudioDeviceInfo info: deviceInfos) {
switch (info.getType()) {
case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
devices.add(AudioModeModule.DEVICE_BLUETOOTH);
break;
case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
devices.add(AudioModeModule.DEVICE_EARPIECE);
break;
case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
devices.add(AudioModeModule.DEVICE_SPEAKER);
break;
case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
case AudioDeviceInfo.TYPE_WIRED_HEADSET:
case TYPE_USB_HEADSET:
devices.add(AudioModeModule.DEVICE_HEADPHONES);
break;
}
}
module.replaceDevices(devices);
JitsiMeetLogger.i(TAG + " Available audio devices: " + devices.toString());
module.updateAudioRoute();
}
};
private final android.media.AudioDeviceCallback audioDeviceCallback =
new android.media.AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(
AudioDeviceInfo[] addedDevices) {
JitsiMeetLogger.d(TAG + " Audio devices added");
onAudioDeviceChange();
}
@Override
public void onAudioDevicesRemoved(
AudioDeviceInfo[] removedDevices) {
JitsiMeetLogger.d(TAG + " Audio devices removed");
onAudioDeviceChange();
}
};
public AudioDeviceHandlerGeneric() {
}
/**
* Helper method to trigger an audio route update when devices change. It
* makes sure the operation is performed on the audio thread.
*/
private void onAudioDeviceChange() {
module.runInAudioThread(onAudioDeviceChangeRunner);
}
/**
* {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
* when the audio focus of the system is updated.
*
* @param focusChange - The type of focus change.
*/
@Override
public void onAudioFocusChange(final int focusChange) {
module.runInAudioThread(new Runnable() {
@Override
public void run() {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN: {
JitsiMeetLogger.d(TAG + " Audio focus gained");
// Some other application potentially stole our audio focus
// temporarily. Restore our mode.
if (audioFocusLost) {
module.updateAudioRoute();
}
audioFocusLost = false;
break;
}
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
JitsiMeetLogger.d(TAG + " Audio focus lost");
audioFocusLost = true;
break;
}
}
}
});
}
/**
* Helper method to set the output route to a Bluetooth device.
*
* @param enabled true if Bluetooth should use used, false otherwise.
*/
private void setBluetoothAudioRoute(boolean enabled) {
if (enabled) {
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
} else {
audioManager.setBluetoothScoOn(false);
audioManager.stopBluetoothSco();
}
}
@Override
public void start(Context context, AudioModeModule audioModeModule) {
JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
module = audioModeModule;
audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
// Setup runtime device change detection.
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
// Do an initial detection.
onAudioDeviceChange();
}
@Override
public void stop() {
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback);
}
@Override
public void setAudioRoute(String device) {
// Turn speaker on / off
audioManager.setSpeakerphoneOn(device.equals(AudioModeModule.DEVICE_SPEAKER));
// Turn bluetooth on / off
setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
}
@Override
public boolean setMode(int mode) {
if (mode == AudioModeModule.DEFAULT) {
audioFocusLost = false;
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.abandonAudioFocus(this);
audioManager.setSpeakerphoneOn(false);
setBluetoothAudioRoute(false);
return true;
}
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setMicrophoneMute(false);
if (audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN)
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
JitsiMeetLogger.w(TAG + " Audio focus request failed");
return false;
}
return true;
}
}

View File

@@ -0,0 +1,229 @@
/*
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
/**
* {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
* legacy (pre-M) Android versions.
*/
class AudioDeviceHandlerLegacy implements
AudioModeModule.AudioDeviceHandlerInterface,
AudioManager.OnAudioFocusChangeListener,
BluetoothHeadsetMonitor.Listener {
private final static String TAG = AudioDeviceHandlerLegacy.class.getSimpleName();
/**
* Reference to the main {@code AudioModeModule}.
*/
private AudioModeModule module;
/**
* Indicator that we have lost audio focus.
*/
private boolean audioFocusLost = false;
/**
* {@link AudioManager} instance used to interact with the Android audio
* subsystem.
*/
private AudioManager audioManager;
/**
* {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in
* old (< M) Android versions.
*/
private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
public AudioDeviceHandlerLegacy() {
}
/**
* Helper method to trigger an audio route update when Bluetooth devices are
* connected / disconnected.
*/
@Override
public void onBluetoothDeviceChange(final boolean deviceAvailable) {
module.runInAudioThread(new Runnable() {
@Override
public void run() {
if (deviceAvailable) {
module.addDevice(AudioModeModule.DEVICE_BLUETOOTH);
} else {
module.removeDevice(AudioModeModule.DEVICE_BLUETOOTH);
}
module.updateAudioRoute();
}
});
}
/**
* Helper method to trigger an audio route update when a headset is plugged
* or unplugged.
*/
private void onHeadsetDeviceChange() {
module.runInAudioThread(new Runnable() {
@Override
public void run() {
// XXX: isWiredHeadsetOn is not deprecated when used just for
// knowing if there is a wired headset connected, regardless of
// audio being routed to it.
//noinspection deprecation
if (audioManager.isWiredHeadsetOn()) {
module.addDevice(AudioModeModule.DEVICE_HEADPHONES);
} else {
module.removeDevice(AudioModeModule.DEVICE_HEADPHONES);
}
module.updateAudioRoute();
}
});
}
/**
* {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
* when the audio focus of the system is updated.
*
* @param focusChange - The type of focus change.
*/
@Override
public void onAudioFocusChange(final int focusChange) {
module.runInAudioThread(new Runnable() {
@Override
public void run() {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN: {
JitsiMeetLogger.d(TAG + " Audio focus gained");
// Some other application potentially stole our audio focus
// temporarily. Restore our mode.
if (audioFocusLost) {
module.updateAudioRoute();
}
audioFocusLost = false;
break;
}
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
JitsiMeetLogger.d(TAG + " Audio focus lost");
audioFocusLost = true;
break;
}
}
}
});
}
/**
* Helper method to set the output route to a Bluetooth device.
*
* @param enabled true if Bluetooth should use used, false otherwise.
*/
private void setBluetoothAudioRoute(boolean enabled) {
if (enabled) {
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
} else {
audioManager.setBluetoothScoOn(false);
audioManager.stopBluetoothSco();
}
}
@Override
public void start(Context context, AudioModeModule audioModeModule) {
JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
module = audioModeModule;
audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
// Setup runtime device change detection.
//
// Detect changes in wired headset connections.
IntentFilter wiredHeadSetFilter = new IntentFilter(AudioManager.ACTION_HEADSET_PLUG);
BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
JitsiMeetLogger.d(TAG + " Wired headset added / removed");
onHeadsetDeviceChange();
}
};
context.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
// Detect Bluetooth device changes.
bluetoothHeadsetMonitor = new BluetoothHeadsetMonitor(context, this);
// On Android < M, detect if we have an earpiece.
PackageManager pm = context.getPackageManager();
if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
module.addDevice(AudioModeModule.DEVICE_EARPIECE);
}
// Always assume there is a speaker.
module.addDevice(AudioModeModule.DEVICE_SPEAKER);
}
@Override
public void stop() {
bluetoothHeadsetMonitor.stop();
bluetoothHeadsetMonitor = null;
}
@Override
public void setAudioRoute(String device) {
// Turn speaker on / off
audioManager.setSpeakerphoneOn(device.equals(AudioModeModule.DEVICE_SPEAKER));
// Turn bluetooth on / off
setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
}
@Override
public boolean setMode(int mode) {
if (mode == AudioModeModule.DEFAULT) {
audioFocusLost = false;
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.abandonAudioFocus(this);
audioManager.setSpeakerphoneOn(false);
setBluetoothAudioRoute(false);
return true;
}
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setMicrophoneMute(false);
if (audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN)
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
JitsiMeetLogger.w(TAG + " Audio focus request failed");
return false;
}
return true;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,16 +16,8 @@
package org.jitsi.meet.sdk;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import androidx.annotation.RequiresApi;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
@@ -61,9 +53,7 @@ import java.util.concurrent.Executors;
* {@code AudioModeModule.DEFAULT} mode should be used.
*/
@ReactModule(name = AudioModeModule.NAME)
class AudioModeModule extends ReactContextBaseJavaModule
implements AudioManager.OnAudioFocusChangeListener {
class AudioModeModule extends ReactContextBaseJavaModule {
public static final String NAME = "AudioMode";
/**
@@ -75,161 +65,33 @@ class AudioModeModule extends ReactContextBaseJavaModule
* - VIDEO_CALL: Used for video calls. It will use the speaker by default,
* unless a wired or Bluetooth headset is connected.
*/
private static final int DEFAULT = 0;
private static final int AUDIO_CALL = 1;
private static final int VIDEO_CALL = 2;
/**
* Constant defining the action for plugging in a headset. This is used on
* our device detection system for API < 23.
*/
private static final String ACTION_HEADSET_PLUG
= (Build.VERSION.SDK_INT >= 21)
? AudioManager.ACTION_HEADSET_PLUG
: Intent.ACTION_HEADSET_PLUG;
/**
* Constant defining a USB headset. Only available on API level >= 26.
* The value of: AudioDeviceInfo.TYPE_USB_HEADSET
*/
private static final int TYPE_USB_HEADSET = 22;
static final int DEFAULT = 0;
static final int AUDIO_CALL = 1;
static final int VIDEO_CALL = 2;
/**
* The {@code Log} tag {@code AudioModeModule} is to log messages with.
*/
static final String TAG = NAME;
/**
* Converts any of the "DEVICE_" constants into the corresponding
* {@link android.telecom.CallAudioState} "ROUTE_" number.
*
* @param audioDevice one of the "DEVICE_" constants.
* @return a route number {@link android.telecom.CallAudioState#ROUTE_EARPIECE} if
* no match is found.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static int audioDeviceToRouteInt(String audioDevice) {
if (audioDevice == null) {
return android.telecom.CallAudioState.ROUTE_EARPIECE;
}
switch (audioDevice) {
case DEVICE_BLUETOOTH:
return android.telecom.CallAudioState.ROUTE_BLUETOOTH;
case DEVICE_EARPIECE:
return android.telecom.CallAudioState.ROUTE_EARPIECE;
case DEVICE_HEADPHONES:
return android.telecom.CallAudioState.ROUTE_WIRED_HEADSET;
case DEVICE_SPEAKER:
return android.telecom.CallAudioState.ROUTE_SPEAKER;
default:
JitsiMeetLogger.e(TAG + " Unsupported device name: " + audioDevice);
return android.telecom.CallAudioState.ROUTE_EARPIECE;
}
}
/**
* Populates given route mask into the "DEVICE_" list.
*
* @param supportedRouteMask an integer coming from
* {@link android.telecom.CallAudioState#getSupportedRouteMask()}.
* @return a list of device names.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static Set<String> routesToDeviceNames(int supportedRouteMask) {
Set<String> devices = new HashSet<>();
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_EARPIECE)
== android.telecom.CallAudioState.ROUTE_EARPIECE) {
devices.add(DEVICE_EARPIECE);
}
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_BLUETOOTH)
== android.telecom.CallAudioState.ROUTE_BLUETOOTH) {
devices.add(DEVICE_BLUETOOTH);
}
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_SPEAKER)
== android.telecom.CallAudioState.ROUTE_SPEAKER) {
devices.add(DEVICE_SPEAKER);
}
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_WIRED_HEADSET)
== android.telecom.CallAudioState.ROUTE_WIRED_HEADSET) {
devices.add(DEVICE_HEADPHONES);
}
return devices;
}
/**
* Whether or not the ConnectionService is used for selecting audio devices.
*/
private static final boolean supportsConnectionService = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
private static boolean useConnectionService_ = supportsConnectionService;
static boolean useConnectionService() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
return supportsConnectionService && useConnectionService_;
}
/**
* Indicator that we have lost audio focus.
*/
private boolean audioFocusLost = false;
/**
* {@link AudioManager} instance used to interact with the Android audio
* subsystem.
*/
private final AudioManager audioManager;
/**
* {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in
* old (< M) Android versions.
*/
private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
private AudioDeviceHandlerInterface audioDeviceHandler;
/**
* {@link ExecutorService} for running all audio operations on a dedicated
* thread.
*/
private static final ExecutorService executor
= Executors.newSingleThreadExecutor();
/**
* {@link Runnable} for running audio device detection the main thread.
* This is only used on Android >= M.
*/
private final Runnable onAudioDeviceChangeRunner = new Runnable() {
@TargetApi(Build.VERSION_CODES.M)
@Override
public void run() {
Set<String> devices = new HashSet<>();
AudioDeviceInfo[] deviceInfos
= audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
for (AudioDeviceInfo info: deviceInfos) {
switch (info.getType()) {
case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
devices.add(DEVICE_BLUETOOTH);
break;
case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
devices.add(DEVICE_EARPIECE);
break;
case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
devices.add(DEVICE_SPEAKER);
break;
case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
case AudioDeviceInfo.TYPE_WIRED_HEADSET:
case TYPE_USB_HEADSET:
devices.add(DEVICE_HEADPHONES);
break;
}
}
availableDevices = devices;
JitsiMeetLogger.i(TAG + " Available audio devices: " +
availableDevices.toString());
// Reset user selection
userSelectedDevice = null;
if (mode != -1) {
updateAudioRoute(mode);
}
}
};
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
/**
* Audio mode currently in use.
@@ -239,10 +101,10 @@ class AudioModeModule extends ReactContextBaseJavaModule
/**
* Audio device types.
*/
private static final String DEVICE_BLUETOOTH = "BLUETOOTH";
private static final String DEVICE_EARPIECE = "EARPIECE";
private static final String DEVICE_HEADPHONES = "HEADPHONES";
private static final String DEVICE_SPEAKER = "SPEAKER";
static final String DEVICE_BLUETOOTH = "BLUETOOTH";
static final String DEVICE_EARPIECE = "EARPIECE";
static final String DEVICE_HEADPHONES = "HEADPHONES";
static final String DEVICE_SPEAKER = "SPEAKER";
/**
* Device change event.
@@ -259,15 +121,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
*/
private String selectedDevice;
/**
* Used on API >= 26 to store the most recently reported audio devices.
* Makes it easier to compare for a change, because the devices are stored
* as a mask in the {@link android.telecom.CallAudioState}. The mask is populated into
* the {@link #availableDevices} on each update.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private int supportedRouteMask;
/**
* User selected device. When null the default is used depending on the
* mode.
@@ -283,31 +136,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
*/
public AudioModeModule(ReactApplicationContext reactContext) {
super(reactContext);
audioManager
= (AudioManager)
reactContext.getSystemService(Context.AUDIO_SERVICE);
// Starting Oreo the ConnectionImpl from ConnectionService is used to
// detect the available devices.
if (!useConnectionService()) {
// Setup runtime device change detection.
setupAudioRouteChangeDetection();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Do an initial detection on Android >= M.
onAudioDeviceChange();
} else {
// On Android < M, detect if we have an earpiece.
PackageManager pm = reactContext.getPackageManager();
if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
availableDevices.add(DEVICE_EARPIECE);
}
// Always assume there is a speaker.
availableDevices.add(DEVICE_SPEAKER);
}
}
}
/**
@@ -364,132 +192,36 @@ class AudioModeModule extends ReactContextBaseJavaModule
}
/**
* Helper method to trigger an audio route update when devices change. It
* makes sure the operation is performed on the main thread.
*
* Only used on Android >= M.
*/
void onAudioDeviceChange() {
runInAudioThread(onAudioDeviceChangeRunner);
}
/**
* Helper method to trigger an audio route update when Bluetooth devices are
* connected / disconnected.
*
* Only used on Android < M. Runs on the main thread.
*/
void onBluetoothDeviceChange() {
if (bluetoothHeadsetMonitor != null && bluetoothHeadsetMonitor.isHeadsetAvailable()) {
availableDevices.add(DEVICE_BLUETOOTH);
} else {
availableDevices.remove(DEVICE_BLUETOOTH);
}
if (mode != -1) {
updateAudioRoute(mode);
}
}
/**
* Helper method to trigger an audio route update when a headset is plugged
* or unplugged.
*
* Only used on Android < M.
*/
void onHeadsetDeviceChange() {
runInAudioThread(new Runnable() {
@Override
public void run() {
// XXX: isWiredHeadsetOn is not deprecated when used just for
// knowing if there is a wired headset connected, regardless of
// audio being routed to it.
//noinspection deprecation
if (audioManager.isWiredHeadsetOn()) {
availableDevices.add(DEVICE_HEADPHONES);
} else {
availableDevices.remove(DEVICE_HEADPHONES);
}
if (mode != -1) {
updateAudioRoute(mode);
}
}
});
}
@RequiresApi(api = Build.VERSION_CODES.O)
void onCallAudioStateChange(Object callAudioState_) {
final android.telecom.CallAudioState callAudioState
= (android.telecom.CallAudioState)callAudioState_;
runInAudioThread(new Runnable() {
@Override
public void run() {
int newSupportedRoutes = callAudioState.getSupportedRouteMask();
boolean audioDevicesChanged
= supportedRouteMask != newSupportedRoutes;
if (audioDevicesChanged) {
supportedRouteMask = newSupportedRoutes;
availableDevices = routesToDeviceNames(supportedRouteMask);
JitsiMeetLogger.i(TAG + " Available audio devices: "
+ availableDevices.toString());
}
boolean audioRouteChanged
= audioDeviceToRouteInt(selectedDevice)
!= callAudioState.getRoute();
if (audioRouteChanged || audioDevicesChanged) {
// Reset user selection
userSelectedDevice = null;
// If the OS changes the Audio Route or Devices we could have lost
// the selected audio device
selectedDevice = null;
if (mode != -1) {
updateAudioRoute(mode);
}
}
}
});
}
/**
* {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
* when the audio focus of the system is updated.
*
* @param focusChange - The type of focus change.
* Initializes the audio device handler module. This function is called *after* all Catalyst
* modules have been created, and that's why we use it, because {@link AudioDeviceHandlerConnectionService}
* needs access to another Catalyst module, so doing this in the constructor would be too early.
*/
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN: {
JitsiMeetLogger.d(TAG + " Audio focus gained");
// Some other application potentially stole our audio focus
// temporarily. Restore our mode.
if (audioFocusLost) {
updateAudioRoute(mode);
}
audioFocusLost = false;
break;
}
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
JitsiMeetLogger.d(TAG + " Audio focus lost");
audioFocusLost = true;
break;
public void initialize() {
setAudioDeviceHandler();
}
private void setAudioDeviceHandler() {
if (audioDeviceHandler != null) {
audioDeviceHandler.stop();
}
if (useConnectionService()) {
audioDeviceHandler = new AudioDeviceHandlerConnectionService();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioDeviceHandler = new AudioDeviceHandlerGeneric();
} else {
audioDeviceHandler = new AudioDeviceHandlerLegacy();
}
audioDeviceHandler.start(getReactApplicationContext(), this);
}
/**
* Helper function to run operations on a dedicated thread.
* @param runnable
*/
public void runInAudioThread(Runnable runnable) {
void runInAudioThread(Runnable runnable) {
executor.execute(runnable);
}
@@ -518,46 +250,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
});
}
/**
* The API >= 26 way of adjusting the audio route.
*
* @param audioDevice one of the "DEVICE_" names to set as the audio route.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private void setAudioRoute(String audioDevice) {
int newAudioRoute = audioDeviceToRouteInt(audioDevice);
RNConnectionService.setAudioRoute(newAudioRoute);
}
/**
* The API < 26 way of adjusting the audio route.
*
* @param audioDevice one of the "DEVICE_" names to set as the audio route.
*/
private void setAudioRoutePreO(String audioDevice) {
// Turn bluetooth on / off
setBluetoothAudioRoute(audioDevice.equals(DEVICE_BLUETOOTH));
// Turn speaker on / off
audioManager.setSpeakerphoneOn(audioDevice.equals(DEVICE_SPEAKER));
}
/**
* Helper method to set the output route to a Bluetooth device.
*
* @param enabled true if Bluetooth should use used, false otherwise.
*/
private void setBluetoothAudioRoute(boolean enabled) {
if (enabled) {
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
} else {
audioManager.setBluetoothScoOn(false);
audioManager.stopBluetoothSco();
}
}
/**
* Public method to set the current audio mode.
*
@@ -587,70 +279,22 @@ class AudioModeModule extends ReactContextBaseJavaModule
AudioModeModule.this.mode = mode;
promise.resolve(null);
} else {
promise.reject(
"setMode",
"Failed to set audio mode to " + mode);
promise.reject("setMode", "Failed to set audio mode to " + mode);
}
}
});
}
/**
* Setup the audio route change detection mechanism. We use the
* {@link android.media.AudioDeviceCallback} on 23 >= Android API < 26.
* Sets whether ConnectionService should be used (if available) for setting the audio mode
* or not.
*
* @param use Boolean indicator of where it should be used or not.
*/
private void setupAudioRouteChangeDetection() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setupAudioRouteChangeDetectionM();
} else {
setupAudioRouteChangeDetectionPreM();
}
}
/**
* Audio route change detection mechanism for 23 >= Android API < 26.
*/
@TargetApi(Build.VERSION_CODES.M)
private void setupAudioRouteChangeDetectionM() {
android.media.AudioDeviceCallback audioDeviceCallback =
new android.media.AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(
AudioDeviceInfo[] addedDevices) {
JitsiMeetLogger.d(TAG + " Audio devices added");
onAudioDeviceChange();
}
@Override
public void onAudioDevicesRemoved(
AudioDeviceInfo[] removedDevices) {
JitsiMeetLogger.d(TAG + " Audio devices removed");
onAudioDeviceChange();
}
};
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
}
/**
* Audio route change detection mechanism for Android API < 23.
*/
private void setupAudioRouteChangeDetectionPreM() {
Context context = getReactApplicationContext();
// Detect changes in wired headset connections.
IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG);
BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
JitsiMeetLogger.d(TAG + " Wired headset added / removed");
onHeadsetDeviceChange();
}
};
context.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
// Detect Bluetooth device changes.
bluetoothHeadsetMonitor = new BluetoothHeadsetMonitor(this, context);
@ReactMethod
public void setUseConnectionService(boolean use) {
useConnectionService_ = use;
setAudioDeviceHandler();
}
/**
@@ -663,14 +307,11 @@ class AudioModeModule extends ReactContextBaseJavaModule
private boolean updateAudioRoute(int mode) {
JitsiMeetLogger.i(TAG + " Update audio route for mode: " + mode);
if (!audioDeviceHandler.setMode(mode)) {
return false;
}
if (mode == DEFAULT) {
if (!useConnectionService()) {
audioFocusLost = false;
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.abandonAudioFocus(this);
audioManager.setSpeakerphoneOn(false);
setBluetoothAudioRoute(false);
}
selectedDevice = null;
userSelectedDevice = null;
@@ -678,20 +319,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
return true;
}
if (!useConnectionService()) {
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setMicrophoneMute(false);
if (audioManager.requestAudioFocus(
this,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN)
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
JitsiMeetLogger.w(TAG + " Audio focus request failed");
return false;
}
}
boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
boolean headsetAvailable = availableDevices.contains(DEVICE_HEADPHONES);
@@ -706,8 +333,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
}
// Consider the user's selection
if (userSelectedDevice != null
&& availableDevices.contains(userSelectedDevice)) {
if (userSelectedDevice != null && availableDevices.contains(userSelectedDevice)) {
audioDevice = userSelectedDevice;
}
@@ -720,13 +346,97 @@ class AudioModeModule extends ReactContextBaseJavaModule
selectedDevice = audioDevice;
JitsiMeetLogger.i(TAG + " Selected audio device: " + audioDevice);
if (useConnectionService()) {
setAudioRoute(audioDevice);
} else {
setAudioRoutePreO(audioDevice);
}
audioDeviceHandler.setAudioRoute(audioDevice);
notifyDevicesChanged();
return true;
}
/**
* Gets the currently selected audio device.
*
* @return The selected audio device.
*/
String getSelectedDevice() {
return selectedDevice;
}
/**
* Resets the current device selection.
*/
void resetSelectedDevice() {
selectedDevice = null;
userSelectedDevice = null;
}
/**
* Adds a new device to the list of available devices.
*
* @param device The new device.
*/
void addDevice(String device) {
availableDevices.add(device);
resetSelectedDevice();
}
/**
* Removes a device from the list of available devices.
*
* @param device The old device to the removed.
*/
void removeDevice(String device) {
availableDevices.remove(device);
resetSelectedDevice();
}
/**
* Replaces the current list of available devices with a new one.
*
* @param devices The new devices list.
*/
void replaceDevices(Set<String> devices) {
availableDevices = devices;
resetSelectedDevice();
}
/**
* Re-sets the current audio route. Needed when devices changes have happened.
*/
void updateAudioRoute() {
if (mode != -1) {
updateAudioRoute(mode);
}
}
/**
* Interface for the modules implementing the actual audio device management.
*/
interface AudioDeviceHandlerInterface {
/**
* Start detecting audio device changes.
* @param context Android {@link Context} where detection should take place.
* @param audioModeModule Reference to the main {@link AudioModeModule}.
*/
void start(Context context, AudioModeModule audioModeModule);
/**
* Stop audio device detection.
*/
void stop();
/**
* Set the appropriate route for the given audio device.
*
* @param device Audio device for which the route must be set.
*/
void setAudioRoute(String device);
/**
* Set the given audio mode.
*
* @param mode The new audio mode to be used.
* @return Whether the operation was successful or not.
*/
boolean setMode(int mode);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,68 +33,43 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
* about device changes when this occurs.
*/
class BluetoothHeadsetMonitor {
/**
* {@link AudioModeModule} where this monitor reports.
*/
private final AudioModeModule audioModeModule;
private final static String TAG = BluetoothHeadsetMonitor.class.getSimpleName();
/**
* The {@link Context} in which {@link #audioModeModule} executes.
* The {@link Context} in which this module executes.
*/
private final Context context;
/**
* Reference to the {@link BluetoothAdapter} object, used to access Bluetooth functionality.
*/
private BluetoothAdapter adapter;
/**
* Reference to a proxy object which allows us to query connected devices.
*/
private BluetoothHeadset headset;
/**
* Flag indicating if there are any Bluetooth headset devices currently
* available.
* receiver registered for receiving Bluetooth connection state changes.
*/
private boolean headsetAvailable = false;
private BroadcastReceiver receiver;
/**
* Helper for running Bluetooth operations on the main thread.
* Listener for receiving Bluetooth device change events.
*/
private final Runnable updateDevicesRunnable
= new Runnable() {
@Override
public void run() {
headsetAvailable
= (headset != null)
&& !headset.getConnectedDevices().isEmpty();
audioModeModule.onBluetoothDeviceChange();
}
};
private Listener listener;
public BluetoothHeadsetMonitor(
AudioModeModule audioModeModule,
Context context) {
this.audioModeModule = audioModeModule;
public BluetoothHeadsetMonitor(Context context, Listener listener) {
this.context = context;
AudioManager audioManager
= (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (!audioManager.isBluetoothScoAvailableOffCall()) {
JitsiMeetLogger.w(AudioModeModule.TAG + " Bluetooth SCO is not available");
return;
}
if (getBluetoothHeadsetProfileProxy()) {
registerBluetoothReceiver();
// Initial detection.
updateDevices();
}
this.listener = listener;
}
private boolean getBluetoothHeadsetProfileProxy() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
JitsiMeetLogger.w(AudioModeModule.TAG + " Device doesn't support Bluetooth");
JitsiMeetLogger.w(TAG + " Device doesn't support Bluetooth");
return false;
}
@@ -104,9 +79,7 @@ class BluetoothHeadsetMonitor {
BluetoothProfile.ServiceListener listener
= new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(
int profile,
BluetoothProfile proxy) {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (profile == BluetoothProfile.HEADSET) {
headset = (BluetoothHeadset) proxy;
updateDevices();
@@ -120,21 +93,7 @@ class BluetoothHeadsetMonitor {
}
};
return
adapter.getProfileProxy(
context,
listener,
BluetoothProfile.HEADSET);
}
/**
* Returns the current headset availability.
*
* @return {@code true} if there is a Bluetooth headset connected;
* {@code false}, otherwise.
*/
public boolean isHeadsetAvailable() {
return headsetAvailable;
return adapter.getProfileProxy(context, listener, BluetoothProfile.HEADSET);
}
private void onBluetoothReceiverReceive(Context context, Intent intent) {
@@ -149,7 +108,7 @@ class BluetoothHeadsetMonitor {
switch (state) {
case BluetoothHeadset.STATE_CONNECTED:
case BluetoothHeadset.STATE_DISCONNECTED:
JitsiMeetLogger.d(AudioModeModule.TAG + " BT headset connection state changed: " + state);
JitsiMeetLogger.d(TAG + " BT headset connection state changed: " + state);
updateDevices();
break;
}
@@ -157,13 +116,12 @@ class BluetoothHeadsetMonitor {
// XXX: This action will be fired when the connection established
// with a Bluetooth headset (called a SCO connection) changes state.
// When the SCO connection is active we route audio to it.
int state
= intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99);
int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99);
switch (state) {
case AudioManager.SCO_AUDIO_STATE_CONNECTED:
case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
JitsiMeetLogger.d(AudioModeModule.TAG + " BT SCO connection state changed: " + state);
JitsiMeetLogger.d(TAG + " BT SCO connection state changed: " + state);
updateDevices();
break;
}
@@ -171,24 +129,63 @@ class BluetoothHeadsetMonitor {
}
private void registerBluetoothReceiver() {
BroadcastReceiver receiver = new BroadcastReceiver() {
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onBluetoothReceiverReceive(context, intent);
}
};
IntentFilter filter = new IntentFilter();
IntentFilter filter = new IntentFilter();
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
context.registerReceiver(receiver, filter);
}
/**
* Detects if there are new devices connected / disconnected and fires the
* {@link AudioModeModule#onAudioDeviceChange()} callback.
* {@link Listener} registered event.
*/
private void updateDevices() {
audioModeModule.runInAudioThread(updateDevicesRunnable);
boolean headsetAvailable = (headset != null) && !headset.getConnectedDevices().isEmpty();
listener.onBluetoothDeviceChange(headsetAvailable);
}
public void start() {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (!audioManager.isBluetoothScoAvailableOffCall()) {
JitsiMeetLogger.w(TAG + " Bluetooth SCO is not available");
return;
}
if (!getBluetoothHeadsetProfileProxy()) {
JitsiMeetLogger.w(TAG + " Couldn't get BT profile proxy");
return;
}
registerBluetoothReceiver();
// Initial detection.
updateDevices();
}
public void stop() {
if (receiver != null) {
context.unregisterReceiver(receiver);
}
if (adapter != null && headset != null) {
adapter.closeProfileProxy(BluetoothProfile.HEADSET, headset);
}
receiver = null;
headset = null;
adapter = null;
}
interface Listener {
void onBluetoothDeviceChange(boolean deviceAvailable);
}
}

View File

@@ -403,11 +403,9 @@ public class ConnectionService extends android.telecom.ConnectionService {
@Override
public void onCallAudioStateChanged(CallAudioState state) {
JitsiMeetLogger.d(TAG + " onCallAudioStateChanged: " + state);
AudioModeModule audioModeModule
= ReactInstanceManagerHolder
.getNativeModule(AudioModeModule.class);
if (audioModeModule != null) {
audioModeModule.onCallAudioStateChange(state);
RNConnectionService module = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
if (module != null) {
module.onCallAudioStateChange(state);
}
}

View File

@@ -29,13 +29,18 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactModule(name = RNConnectionService.NAME)
class RNConnectionService
extends ReactContextBaseJavaModule {
class RNConnectionService extends ReactContextBaseJavaModule {
public static final String NAME = "ConnectionService";
private static final String TAG = ConnectionService.TAG;
/**
* Handler for dealing with call state changes. We are acting as a proxy between ConnectionService
* and other modules such as {@link AudioModeModule}.
*/
private CallAudioStateListener callAudioStateListener;
/**
* Sets the audio route on all existing {@link android.telecom.Connection}s
*
@@ -168,4 +173,28 @@ class RNConnectionService
public void updateCall(String callUUID, ReadableMap callState) {
ConnectionService.updateCall(callUUID, callState);
}
public CallAudioStateListener getCallAudioStateListener() {
return callAudioStateListener;
}
public void setCallAudioStateListener(CallAudioStateListener callAudioStateListener) {
this.callAudioStateListener = callAudioStateListener;
}
/**
* Handler for call state changes. {@code ConnectionServiceImpl} will call this handler when the
* call audio state changes.
*
* @param callAudioState The current call's audio state.
*/
void onCallAudioStateChange(android.telecom.CallAudioState callAudioState) {
if (callAudioStateListener != null) {
callAudioStateListener.onCallAudioStateChange(callAudioState);
}
}
interface CallAudioStateListener {
void onCallAudioStateChange(android.telecom.CallAudioState callAudioState);
}
}

View File

@@ -419,9 +419,15 @@ var config = {
// the menu has option to flip the locally seen video for local presentations
// disableLocalVideoFlip: false
// If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for
// user documentation.
// userDocumentationURL: 'https://docs.example.com/video-meetings.html'
// Deployment specific URLs.
// deploymentUrls: {
// // If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for
// // user documentation.
// userDocumentationURL: 'https://docs.example.com/video-meetings.html',
// // If specified a 'Download our apps' button will be displayed in the overflow menu with a link
// // to the specified URL for an app download page.
// downloadAppsUrl: 'https://docs.example.com/our-apps.html'
// }
// List of undocumented settings used in jitsi-meet
/**

View File

@@ -82,9 +82,10 @@
#chat-recipient {
align-items: center;
background-color: $defaultWarningColor;
background-color: $chatPrivateMessageBackgroundColor;
display: flex;
flex-direction: row;
font-weight: 100;
padding: 10px;
span {
@@ -132,6 +133,7 @@
#chat-input {
border-top: 1px solid $chatInputSeparatorColor;
display: flex;
padding: 5px 10px;
* {
background-color: transparent;
@@ -152,8 +154,7 @@
box-shadow: none;
color: white;
font-size: 15px;
line-height: 30px;
padding: 5px;
padding: 10px;
overflow-y: auto;
resize: none;
width: 100%;
@@ -183,6 +184,7 @@
.display-name {
font-size: 13px;
font-weight: bold;
margin-bottom: 5px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -196,7 +198,6 @@
color: white;
margin-top: 3px;
max-width: 100%;
padding-bottom: 3px;
position: relative;
&.localuser {
@@ -219,8 +220,12 @@
}
.privatemessagenotice {
color: $defaultWarningColor;
font-style: italic;
font-size: 11px;
font-weight: 100;
}
.messagecontent {
margin: 5px 10px;
}
}
@@ -297,10 +302,6 @@
cursor: pointer;
}
#usermsg::-webkit-input-placeholder {
line-height: 30px;
}
#usermsg::-webkit-scrollbar-track-piece {
background: #3a3a3a;
}
@@ -315,6 +316,10 @@
.chatmessage {
background-color: $chatLocalMessageBackgroundColor;
border-radius: 6px 0px 6px 6px;
&.privatemessage {
background-color: $chatPrivateMessageBackgroundColor;
}
}
.display-name {
@@ -328,8 +333,9 @@
&.error {
.chatmessage {
background-color: $defaultWarningColor;
border-radius: 0px;
color: red;
font-weight: 100;
}
.display-name {
@@ -345,8 +351,17 @@
flex-direction: row;
align-items: center;
.toolbox-icon {
cursor: pointer;
.messageactions {
align-self: stretch;
border-left: 1px solid $chatActionsSeparatorColor;
display: flex;
flex-direction: column;
justify-content: center;
padding: 5px;
.toolbox-icon {
cursor: pointer;
}
}
}
}
@@ -357,6 +372,9 @@
display: inline-block;
margin-top: 3px;
color: white;
padding: 8px;
&.privatemessage {
background-color: $chatPrivateMessageBackgroundColor;
}
}
}

66
css/_mini_toolbox.scss Normal file
View File

@@ -0,0 +1,66 @@
.filmstrip-toolbox,
.always-on-top-toolbox {
background-color: $newToolbarBackgroundColor;
border-radius: 3px;
display: flex;
z-index: $toolbarZ;
.toolbox-icon {
cursor: pointer;
padding: 7px;
&.toggled {
background: $AOTToolbarButtonToggleColor;
}
&.disabled {
cursor: initial;
}
}
}
.always-on-top-toolbox {
flex-direction: row;
left: 50%;
position: absolute;
top: 10px;
transform: translateX(-50%);
.toolbox-button {
&:first-child {
.toolbox-icon {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
}
&:nth-child(2) {
svg {
fill: $hangupColor;
}
}
&:last-child {
.toolbox-icon {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
}
}
}
.filmstrip-toolbox {
flex-direction: column;
.toolbox-button {
&:nth-child(1) {
svg {
fill: $hangupColor;
}
}
.toolbox-icon {
border-radius: 3px;
}
}
}

View File

@@ -0,0 +1,26 @@
.participants-count {
background: #fff;
border-radius: 4px;
color: #5e6d7a;
cursor: pointer;
display: inline-block;
font-size: 13px;
line-height: 20px;
margin-left: 16px;
padding: 4px 8px;
pointer-events: auto;
&-number {
margin-right: 8px;
vertical-align: middle;
}
&-icon {
background: url('../images/user-groups.svg');
background-repeat: no-repeat;
display: inline-block;
height: 16px;
width: 16px;
vertical-align: middle;
}
}

View File

@@ -0,0 +1 @@
/** Insert custom CSS for any additional content in the promotional footer **/

View File

@@ -9,7 +9,7 @@
text-align: center;
font-size: 17px;
color: #fff;
z-index: $toolbarBackgroundZ;
z-index: $zindex10;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
@@ -19,4 +19,8 @@
&.visible {
top: 0px;
}
&-text {
vertical-align: middle;
}
}

View File

@@ -270,89 +270,6 @@
}
}
.always-on-top-toolbox,
.filmstrip-toolbox {
background-color: $newToolbarBackgroundColor;
box-sizing: border-box;
display: flex;
flex-direction: column;
z-index: $toolbarZ;
i {
cursor: pointer;
display: block;
}
i:hover {
background-color: $AOTToolbarButtonHoverColor;
}
i.toggled {
background: $AOTToolbarButtonToggleColor;
}
i.toggled:hover:not(.disabled) {
background-color: $AOTToolbarButtonHoverColor;
}
.toolbox-button {
color: $toolbarButtonColor;
cursor: pointer;
text-align: center;
}
border-radius: 3px;
}
.always-on-top-toolbox {
flex-direction: row;
left: 50%;
position: absolute;
top: 10px;
transform: translateX(-50%);
z-index: $toolbarZ;
i {
font-size: $newToolbarFontSize;
height: $newToolbarSize;
line-height: $newToolbarSize;
width: $newToolbarSize;
}
.disabled {
cursor: initial;
}
.toolbox-button:first-child i {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.toolbox-button:last-child i {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
}
.filmstrip-toolbox {
i {
font-size: 1.9em;
height: 37px;
line-height: 37px;
width: 37px;
}
.toolbox-button:first-child i {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.toolbox-button:last-child i {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
}
/**
* START of fade in animation for main toolbar
*/

View File

@@ -86,10 +86,12 @@ $modalTextColor: #333;
/**
* Chat
*/
$chatActionsSeparatorColor: rgb(173, 105, 112);
$chatHeaderBackgroundColor: rgba(42, 58, 75, 0.9);
$chatInputSeparatorColor: #A4B8D1;
$chatLocalMessageBackgroundColor: rgba(26, 108, 180, 1);
$chatRemoteMessageBackgroundColor: rgba(240, 243, 247, 0.15);
$chatLocalMessageBackgroundColor: rgb(4, 98, 178);
$chatPrivateMessageBackgroundColor: rgb(153, 69, 77);
$chatRemoteMessageBackgroundColor: rgb(86, 101, 114);
$sidebarWidth: 375px;
/**
@@ -178,8 +180,17 @@ $welcomePageHeaderTextMarginTop: 35px;
$welcomePageHeaderTextMarginBottom: 35px;
$welcomePageHeaderTextTitleMarginBottom: 16px;
$welcomePageHeaderTextTitleFontSize: 2.5rem;
$welcomePageHeaderTextTitleFontWeight: 500;
$welcomePageHeaderTextTitleLineHeight: 1.18;
$welcomePageHeaderTextTitleOpacity: 1;
$welcomePageHeaderTextDescriptionDisplay: inherit;
$welcomePageHeaderTextDescriptionFontSize: 1rem;
$welcomePageHeaderTextDescriptionFontWeight: 400;
$welcomePageHeaderTextDescriptionLineHeight: 24px;
$welcomePageHeaderTextDescriptionMarginBottom: 20px;
$welcomePageHeaderTextDescriptionAlignSelf: inherit;
$welcomePageEnterRoomWidth: 680px;
$welcomePageEnterRoomPadding: 25px 30px;
@@ -198,6 +209,8 @@ $welcomePageTabButtonsDisplay: flex;
$welcomePageTabDisplay: block;
$welcomePageButtonWidth: 51px;
$welcomePageButtonMinWidth: inherit;
$welcomePageButtonFontSize: 14px;
$welcomePageButtonHeight: 35px;
$welcomePageButtonFontWeight: inherit;
$welcomePageButtonBorderRadius: 4px;
@@ -207,4 +220,39 @@ $welcomePageButtonLineHeight: 35px;
* Deep-linking page variables.
*/
$deepLinkingMobileLogoHeight: 40px;
$deepLinkingMobileHeaderBackground: #f1f2f5;
$deepLinkingMobileLinkColor: inherit;
$deepLinkingMobileTextFontSize: inherit;
$deepLinkingMobileTextLineHeight: inherit;
$deepLinkingDialInConferenceIdMargin: 10px 0 10px 0;
$deepLinkingDialInConferenceIdPadding: inherit;
$deepLinkingDialInConferenceIdBackgroundColor: inherit;
$deepLinkingDialInConferenceIdBorderRadius: inherit;
$deepLinkingDialInConferenceNameFontSize: inherit;
$deepLinkingDialInConferenceNameLineHeight: inherit;
$deepLinkingDialInConferenceNameMarginBottom: none;
$deepLinkingDialInConferenceNameFontWeight: inherit;
$deepLinkingDialInConferenceDescriptionFontSize: 0.8em;
$deepLinkingDialInConferenceDescriptionLineHeight: inherit;
$deepLinkingDialInConferenceDescriptionMarginBottom: none;
$deepLinkingDialInConferencePinFontSize: inherit;
$deepLinkingDialInConferencePinLineHeight: inherit;
$depLinkingMobileHrefLineHeight: 2.2857142857142856em;
$deepLinkingMobileHrefFontWeight: bolder;
$deepLinkingMobileHrefFontSize: inherit;
$deepLinkingMobileButtonHeight: 2.2857142857142856em;
$deepLinkingMobileButtonLineHeight: 2.2857142857142856em;
$deepLinkingMobileButtonMargin: 18px auto 10px;
$deepLinkingMobileButtonWidth: auto;
$deepLinkingMobileButtonFontWeight: bold;
$deepLinkingMobileButtonFontSize: inherit;
$primaryDeepLinkingMobileButtonBorderRadius: inherit;

View File

@@ -38,19 +38,21 @@ body.welcome-page {
.header-text-title {
color: $welcomePageTitleColor;
font-size: 2.5rem;
font-weight: 500;
line-height: 1.18;
font-size: $welcomePageHeaderTextTitleFontSize;
font-weight: $welcomePageHeaderTextTitleFontWeight;
line-height: $welcomePageHeaderTextTitleLineHeight;
margin-bottom: $welcomePageHeaderTextTitleMarginBottom;
opacity: $welcomePageHeaderTextTitleOpacity;
}
.header-text-description {
display: $welcomePageHeaderTextDescriptionDisplay;
color: $welcomePageDescriptionColor;
font-size: 1rem;
font-weight: 400;
line-height: 24px;
margin-bottom: 20px;
font-size: $welcomePageHeaderTextDescriptionFontSize;
font-weight: $welcomePageHeaderTextDescriptionFontWeight;
line-height: $welcomePageHeaderTextDescriptionLineHeight;
margin-bottom: $welcomePageHeaderTextDescriptionMarginBottom;
align-self: $welcomePageHeaderTextDescriptionAlignSelf;
}
#enter_room {
@@ -148,8 +150,9 @@ body.welcome-page {
.welcome-page-button {
width: $welcomePageButtonWidth;
min-width: $welcomePageButtonMinWidth;
height: $welcomePageButtonHeight;
font-size: 14px;
font-size: $welcomePageButtonFontSize;
font-weight: $welcomePageButtonFontWeight;
background: #0074E0;
border-radius: $welcomePageButtonBorderRadius;

View File

@@ -19,7 +19,8 @@
}
a {
text-decoration: none
text-decoration: none;
color: $deepLinkingMobileLinkColor;
}
&__body {
@@ -41,6 +42,8 @@
&__text {
font-weight: bolder;
font-size: $deepLinkingMobileTextFontSize;
line-height: $deepLinkingMobileTextLineHeight;
padding: 10px 10px 0px 10px;
}
@@ -65,11 +68,28 @@
}
.dial-in-conference-id {
margin: 10px 0 10px 0;
margin: $deepLinkingDialInConferenceIdMargin;
padding: $deepLinkingDialInConferenceIdPadding;
background-color: $deepLinkingDialInConferenceIdBackgroundColor;
border-radius: $deepLinkingDialInConferenceIdBorderRadius;
}
.dial-in-conference-name {
font-size: $deepLinkingDialInConferenceNameFontSize;
line-height: $deepLinkingDialInConferenceNameLineHeight;
margin-bottom: $deepLinkingDialInConferenceNameMarginBottom;
font-weight: $deepLinkingDialInConferenceNameFontWeight;
}
.dial-in-conference-description {
font-size: 0.8em;
font-size: $deepLinkingDialInConferenceDescriptionFontSize;
line-height: $deepLinkingDialInConferenceDescriptionLineHeight;
margin-bottom: $deepLinkingDialInConferenceDescriptionMarginBottom;
}
.dial-in-conference-pin {
font-size: $deepLinkingDialInConferencePinFontSize;
line-height: $deepLinkingDialInConferencePinLineHeight;
}
.toll-free-list {
@@ -88,25 +108,27 @@
&__href {
height: 2.2857142857142856em;
line-height: 2.2857142857142856em;
line-height: $depLinkingMobileHrefLineHeight;
margin: 18px auto 20px;
max-width: 300px;
width: auto;
font-weight: bolder;
font-weight: $deepLinkingMobileHrefFontWeight;
font-size: $deepLinkingMobileHrefFontSize;
}
&__button {
border: 0;
height: 2.2857142857142856em;
line-height: 2.2857142857142856em;
margin: 18px auto 10px;
height: $deepLinkingMobileButtonHeight;
line-height: $deepLinkingMobileButtonLineHeight;
margin: $deepLinkingMobileButtonMargin;
padding: 0px 10px 0px 10px;
max-width: 300px;
width: auto;
width: $deepLinkingMobileButtonWidth;
@include border-radius(3px);
background-color: $unsupportedBrowserButtonBgColor;
color: #505F79;
font-weight: bold;
font-weight: $deepLinkingMobileButtonFontWeight;
font-size: $deepLinkingMobileButtonFontSize;
&:active {
background-color: $unsupportedBrowserButtonBgColor;
@@ -115,7 +137,7 @@
&_primary {
background-color: $primaryUnsupportedBrowserButtonBgColor;
color: #FFFFFF;
border-radius: $primaryDeepLinkingMobileButtonBorderRadius;
&:active {
background-color: $primaryUnsupportedBrowserButtonBgColor;
}

View File

@@ -32,6 +32,7 @@ $flagsImagePath: "../images/";
@import 'overlay/overlay';
@import 'inlay';
@import 'reload_overlay/reload_overlay';
@import 'mini_toolbox';
@import 'modals/desktop-picker/desktop-picker';
@import 'modals/device-selection/device-selection';
@import 'modals/dialog';
@@ -44,6 +45,7 @@ $flagsImagePath: "../images/";
@import 'videolayout_default';
@import 'notice';
@import 'subject';
@import 'participants-count';
@import 'popup_menu';
@import 'recording';
@import 'login_menu';
@@ -82,5 +84,6 @@ $flagsImagePath: "../images/";
@import 'third-party-branding/google';
@import 'third-party-branding/microsoft';
@import 'avatar';
@import 'promotional-footer';
/* Modules END */

View File

@@ -63,6 +63,8 @@
width: -webkit-max-content;
word-break: break-all;
max-width: 400px;
display: flex;
align-items: center;
}
.info-dialog-dial-in {
@@ -86,6 +88,15 @@
cursor: inherit;
}
.info-dialog-url-icon {
display: inline-block;
margin-left: 5px;
svg {
cursor: pointer;
}
}
.info-dialog-title {
font-weight: bold;
margin-bottom: 10px;
@@ -214,4 +225,10 @@
-moz-user-select: text;
-webkit-user-select: text;
}
.info-dialog-url-text-unselectable {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
}

View File

@@ -19,29 +19,37 @@ server {
ssl_certificate_key /etc/jitsi/meet/jitsi-meet.example.com.key;
root /usr/share/jitsi-meet;
ssi on;
index index.html index.htm;
error_page 404 /static/404.html;
location /config.js {
location = /config.js {
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
}
location /external_api.js {
location = /external_api.js {
alias /usr/share/jitsi-meet/libs/external_api.min.js;
}
location ~ ^/([a-zA-Z0-9=\?]+)$ {
rewrite ^/(.*)$ / break;
}
location / {
ssi on;
#ensure all static content can always be found first
location ~ ^/(libs|css|static|images|fonts|lang|sounds|connection_optimization|.well-known)/(.*)$
{
add_header 'Access-Control-Allow-Origin' '*';
alias /usr/share/jitsi-meet/$1/$2;
}
# BOSH
location /http-bind {
location = /http-bind {
proxy_pass http://localhost:5280/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
}
location ~ ^/([^?&:’“]+)$ {
try_files $uri @root_path;
}
location @root_path {
rewrite ^/(.*)$ / break;
}
}

View File

@@ -0,0 +1,14 @@
var subdomain = "<!--# echo var="subdomain" default="" -->";
if (subdomain) {
subdomain = subdomain.substr(0,subdomain.length-1).split('.').join('_').toLowerCase() + '.';
}
var config = {
hosts: {
domain: 'jitsi.example.com',
muc: 'conference.'+subdomain+'jitsi.example.com', // FIXME: use XEP-0030
focus: 'focus.jitsi.example.com',
},
useNicks: false,
bosh: '//jitsi.example.com/http-bind' // FIXME: use xep-0156 for that
};

View File

@@ -0,0 +1,67 @@
server {
listen 80;
server_name jitsi.example.com;
# set the root
root /srv/jitsi.example.com;
# ssi on with javascript for multidomain variables in config.js
ssi on;
ssi_types application/x-javascript application/javascript;
index index.html;
set $prefix "";
# BOSH
location /http-bind {
proxy_pass http://localhost:5280/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
}
# xmpp websockets
location /xmpp-websocket {
proxy_pass http://localhost:5280/xmpp-websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
tcp_nodelay on;
}
location ~ ^/([^/?&:'"]+)$ {
try_files $uri @root_path;
}
location @root_path {
rewrite ^/(.*)$ / break;
}
location / {
ssi on;
}
location ~ ^/([^/?&:'"]+)/config.js$
{
set $subdomain "$1.";
set $subdir "$1/";
alias /etc/jitsi/meet/{{jitsi_meet_domain_name}}-config.js;
}
#Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
location ~ ^/([^/?&:'"]+)/(.*)$ {
set $subdomain "$1.";
set $subdir "$1/";
rewrite ^/([^/?&:'"]+)/(.*)$ /$2;
}
# BOSH for subdomains
location ~ ^/([^/?&:'"]+)/http-bind {
set $subdomain "$1.";
set $subdir "$1/";
set $prefix "$1";
rewrite ^/(.*)$ /http-bind;
}
}

View File

@@ -0,0 +1,220 @@
-- Prosody XMPP Server Configuration
--
-- Information on configuring Prosody can be found on our
-- website at http://prosody.im/doc/configure
--
-- Tip: You can check that the syntax of this file is correct
-- when you have finished by running: prosodyctl check config
-- If there are any errors, it will let you know what and where
-- they are, otherwise it will keep quiet.
--
-- Good luck, and happy Jabbering!
---------- Server-wide settings ----------
-- Settings in this section apply to the whole server and are the default settings
-- for any virtual hosts
-- This is a (by default, empty) list of accounts that are admins
-- for the server. Note that you must create the accounts separately
-- (see http://prosody.im/doc/creating_accounts for info)
-- Example: admins = { "user1@example.com", "user2@example.net" }
admins = { }
daemonize = true
cross_domain_bosh = true;
component_ports = { 5347 }
--component_interface = "192.168.0.10"
-- Enable use of libevent for better performance under high load
-- For more information see: http://prosody.im/doc/libevent
--use_libevent = true
-- This is the list of modules Prosody will load on startup.
-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
-- Documentation on modules can be found at: http://prosody.im/doc/modules
modules_enabled = {
-- Generally required
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"dialback"; -- s2s dialback support
"disco"; -- Service discovery
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
-- Not essential, but recommended
"private"; -- Private XML storage (for room bookmarks, etc.)
"vcard"; -- Allow users to set vCards
-- These are commented by default as they have a performance impact
--"privacy"; -- Support privacy lists
"compression"; -- Stream compression (requires the lua-zlib package installed)
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
"pep"; -- Enables users to publish their mood, activity, playing music and more
"register"; -- Allow users to register on this server using a client and change passwords
-- Admin interfaces
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- HTTP modules
"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"http_files"; -- Serve static files from a directory over HTTP
-- Other specific functionality
--"groups"; -- Shared roster support
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
-- jitsi
"smacks";
"carbons";
"mam";
"lastactivity";
"offline";
"pubsub";
"adhoc";
"websocket";
"http_altconnect";
-- include domain mapper as global level module
"muc_domain_mapper";
}
-- domain mapper options, must at least have domain base set to use the mapper
muc_mapper_domain_base = "jitsi.example.com";
-- These modules are auto-loaded, but should you want
-- to disable them then uncomment them here:
modules_disabled = {
-- "offline"; -- Store offline messages
-- "c2s"; -- Handle client connections
-- "s2s"; -- Handle server-to-server connections
}
-- Disable account creation by default, for security
-- For more information see http://prosody.im/doc/creating_accounts
allow_registration = false
-- These are the SSL/TLS-related settings. If you don't want
-- to use SSL/TLS, you may comment or remove this
ssl = {
key = "/etc/prosody/certs/localhost.key";
certificate = "/etc/prosody/certs/localhost.crt";
}
-- Force clients to use encrypted connections? This option will
-- prevent clients from authenticating unless they are using encryption.
-- c2s_require_encryption = true
-- Force certificate authentication for server-to-server connections?
-- This provides ideal security, but requires servers you communicate
-- with to support encryption AND present valid, trusted certificates.
-- NOTE: Your version of LuaSec must support certificate verification!
-- For more information see http://prosody.im/doc/s2s#security
-- s2s_secure_auth = false
-- Many servers don't support encryption or have invalid or self-signed
-- certificates. You can list domains here that will not be required to
-- authenticate using certificates. They will be authenticated using DNS.
--s2s_insecure_domains = { "gmail.com" }
-- Even if you leave s2s_secure_auth disabled, you can still require valid
-- certificates for some domains by specifying a list here.
--s2s_secure_domains = { "jabber.org" }
-- Required for init scripts and prosodyctl
pidfile = "/var/run/prosody/prosody.pid"
-- Select the authentication backend to use. The 'internal' providers
-- use Prosody's configured data storage to store the authentication data.
-- To allow Prosody to offer secure authentication mechanisms to clients, the
-- default provider stores passwords in plaintext. If you do not trust your
-- server please see http://prosody.im/doc/modules/mod_auth_internal_hashed
-- for information about using the hashed backend.
-- authentication = "internal_plain"
authentication = "internal_hashed"
-- Select the storage backend to use. By default Prosody uses flat files
-- in its configured data directory, but it also supports more backends
-- through modules. An "sql" backend is included by default, but requires
-- additional dependencies. See http://prosody.im/doc/storage for more info.
--storage = "sql" -- Default is "internal"
-- For the "sql" backend, you can uncomment *one* of the below to configure:
--sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename.
--sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
--sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
-- Logging configuration
-- For advanced logging see http://prosody.im/doc/logging
log = {
info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
error = "/var/log/prosody/prosody.err";
"*syslog";
}
----------- Virtual hosts -----------
-- You need to add a VirtualHost entry for each domain you wish Prosody to serve.
-- Settings under each VirtualHost entry apply *only* to that host.
--VirtualHost "localhost"
VirtualHost "jitsi.example.com"
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/var/lib/prosody/jitsi.example.com.key";
certificate = "/var/lib/prosody/jitsi.example.com.crt";
}
c2s_require_encryption = false
VirtualHost "auth.jitsi.example.com"
ssl = {
key = "/var/lib/prosody/auth.jitsi.example.com.key";
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
}
authentication = "internal_plain"
------ Components ------
-- You can specify components to add hosts that provide special services,
-- like multi-user conferences, and transports.
-- For more information on components, see http://prosody.im/doc/components
---Set up a MUC (multi-user chat) room server on conference.example.com:
--Component "conference.example.com" "muc"
-- Set up a SOCKS5 bytestream proxy for server-proxied file transfers:
--Component "proxy.example.com" "proxy65"
---Set up an external component (default component port is 5347)
--
-- External components allow adding various services, such as gateways/
-- transports to other networks like ICQ, MSN and Yahoo. For more info
-- see: http://prosody.im/doc/components#adding_an_external_component
--
--Component "gateway.example.com"
-- component_secret = "password"
Component "conference.jitsi.example.com" "muc"
modules_enabled = { "muc_domain_mapper" }
Component "jitsi-videobridge.jitsi.example.com"
component_secret = "IfGaish6"

View File

@@ -210,6 +210,7 @@ Checkout and configure Jitsi Meet:
cd /srv
git clone https://github.com/jitsi/jitsi-meet.git
mv jitsi-meet/ jitsi.example.com
cd jitsi.example.com
npm install
make
```

3
images/user-groups.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33331 2C6.28101 2 7.09675 2.56499 7.46207 3.37651C7.00766 3.45023 6.58406 3.61583 6.21095 3.85361C6.04111 3.54356 5.71176 3.33333 5.33331 3.33333C4.78103 3.33333 4.33331 3.78105 4.33331 4.33333C4.33331 4.75895 4.59921 5.12246 4.97395 5.26682C4.77672 5.69245 4.66665 6.16671 4.66665 6.66667L4.66678 6.6967C3.12249 6.85332 2.66665 7.65415 2.66665 9.83333C2.66665 9.89666 2.66835 9.95222 2.67088 10H3.13441C2.977 10.3982 2.86114 10.8423 2.7841 11.3333H2.33331C1.66665 11.3333 1.33331 10.8333 1.33331 9.83333C1.33331 7.60559 1.88097 6.20498 3.39417 5.63152C3.14521 5.26038 2.99998 4.81382 2.99998 4.33333C2.99998 3.04467 4.04465 2 5.33331 2ZM9.78901 3.85361C9.4159 3.61583 8.9923 3.45023 8.53788 3.37651C8.90321 2.56499 9.71895 2 10.6666 2C11.9553 2 13 3.04467 13 4.33333C13 4.81382 12.8547 5.26038 12.6058 5.63152C14.119 6.20498 14.6666 7.60559 14.6666 9.83333C14.6666 10.8333 14.3333 11.3333 13.6666 11.3333H13.2159C13.1388 10.8423 13.023 10.3982 12.8656 10H13.3291C13.3316 9.95222 13.3333 9.89666 13.3333 9.83333C13.3333 7.65415 12.8775 6.85332 11.3332 6.6967L11.3333 6.66667C11.3333 6.1667 11.2232 5.69245 11.026 5.26682C11.4008 5.12246 11.6666 4.75895 11.6666 4.33333C11.6666 3.78105 11.2189 3.33333 10.6666 3.33333C10.2882 3.33333 9.95885 3.54356 9.78901 3.85361ZM4.49998 14.6667C3.7222 14.6667 3.33331 14.1111 3.33331 13C3.33331 10.4598 4.0062 8.8875 5.87888 8.28308C5.5366 7.83462 5.33331 7.27438 5.33331 6.66667C5.33331 5.19391 6.52722 4 7.99998 4C9.47274 4 10.6666 5.19391 10.6666 6.66667C10.6666 7.27438 10.4634 7.83462 10.1211 8.28308C11.9938 8.8875 12.6666 10.4598 12.6666 13C12.6666 14.1111 12.2778 14.6667 11.5 14.6667H4.49998ZM9.33331 6.66667C9.33331 7.40305 8.73636 8 7.99998 8C7.2636 8 6.66665 7.40305 6.66665 6.66667C6.66665 5.93029 7.2636 5.33333 7.99998 5.33333C8.73636 5.33333 9.33331 5.93029 9.33331 6.66667ZM11.3333 13C11.3333 13.1426 11.3252 13.2536 11.3152 13.3333H4.68477C4.67476 13.2536 4.66665 13.1426 4.66665 13C4.66665 10.1957 5.42021 9.33333 7.99998 9.33333C10.5797 9.33333 11.3333 10.1957 11.3333 13Z" fill="#5E6D7A"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -51,7 +51,7 @@ var interfaceConfig = {
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur'
'tileview', 'videobackgroundblur', 'download', 'help'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

View File

@@ -82,6 +82,7 @@ post_install do |installer|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'YES'
config.build_settings['SUPPORTS_MACCATALYST'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0'
end
end
end

View File

@@ -15,14 +15,14 @@ PODS:
- Fabric (~> 1.9.0)
- DoubleConversion (1.1.6)
- Fabric (1.9.0)
- FBLazyVector (0.61.1)
- FBReactNativeSpec (0.61.1):
- FBLazyVector (0.61.3)
- FBReactNativeSpec (0.61.3):
- Folly (= 2018.10.22.00)
- RCTRequired (= 0.61.1)
- RCTTypeSafety (= 0.61.1)
- React-Core (= 0.61.1)
- React-jsi (= 0.61.1)
- ReactCommon/turbomodule/core (= 0.61.1)
- RCTRequired (= 0.61.3)
- RCTTypeSafety (= 0.61.3)
- React-Core (= 0.61.3)
- React-jsi (= 0.61.3)
- ReactCommon/turbomodule/core (= 0.61.3)
- Firebase/Core (5.18.0):
- Firebase/CoreOnly
- FirebaseAnalytics (= 5.7.0)
@@ -103,169 +103,169 @@ PODS:
- nanopb/decode (0.3.901)
- nanopb/encode (0.3.901)
- ObjectiveDropboxOfficial (3.9.4)
- RCTRequired (0.61.1)
- RCTTypeSafety (0.61.1):
- FBLazyVector (= 0.61.1)
- RCTRequired (0.61.3)
- RCTTypeSafety (0.61.3):
- FBLazyVector (= 0.61.3)
- Folly (= 2018.10.22.00)
- RCTRequired (= 0.61.1)
- React-Core (= 0.61.1)
- React (0.61.1):
- React-Core (= 0.61.1)
- React-Core/DevSupport (= 0.61.1)
- React-Core/RCTWebSocket (= 0.61.1)
- React-RCTActionSheet (= 0.61.1)
- React-RCTAnimation (= 0.61.1)
- React-RCTBlob (= 0.61.1)
- React-RCTImage (= 0.61.1)
- React-RCTLinking (= 0.61.1)
- React-RCTNetwork (= 0.61.1)
- React-RCTSettings (= 0.61.1)
- React-RCTText (= 0.61.1)
- React-RCTVibration (= 0.61.1)
- React-Core (0.61.1):
- RCTRequired (= 0.61.3)
- React-Core (= 0.61.3)
- React (0.61.3):
- React-Core (= 0.61.3)
- React-Core/DevSupport (= 0.61.3)
- React-Core/RCTWebSocket (= 0.61.3)
- React-RCTActionSheet (= 0.61.3)
- React-RCTAnimation (= 0.61.3)
- React-RCTBlob (= 0.61.3)
- React-RCTImage (= 0.61.3)
- React-RCTLinking (= 0.61.3)
- React-RCTNetwork (= 0.61.3)
- React-RCTSettings (= 0.61.3)
- React-RCTText (= 0.61.3)
- React-RCTVibration (= 0.61.3)
- React-Core (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default (= 0.61.1)
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-Core/Default (= 0.61.3)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/CoreModulesHeaders (0.61.1):
- React-Core/CoreModulesHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/Default (0.61.1):
- React-Core/Default (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/DevSupport (0.61.1):
- React-Core/DevSupport (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default (= 0.61.1)
- React-Core/RCTWebSocket (= 0.61.1)
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-jsinspector (= 0.61.1)
- React-Core/Default (= 0.61.3)
- React-Core/RCTWebSocket (= 0.61.3)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- React-jsinspector (= 0.61.3)
- Yoga
- React-Core/RCTActionSheetHeaders (0.61.1):
- React-Core/RCTActionSheetHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/RCTAnimationHeaders (0.61.1):
- React-Core/RCTAnimationHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/RCTBlobHeaders (0.61.1):
- React-Core/RCTBlobHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/RCTImageHeaders (0.61.1):
- React-Core/RCTImageHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/RCTLinkingHeaders (0.61.1):
- React-Core/RCTLinkingHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/RCTNetworkHeaders (0.61.1):
- React-Core/RCTNetworkHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/RCTSettingsHeaders (0.61.1):
- React-Core/RCTSettingsHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/RCTTextHeaders (0.61.1):
- React-Core/RCTTextHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/RCTVibrationHeaders (0.61.1):
- React-Core/RCTVibrationHeaders (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-Core/RCTWebSocket (0.61.1):
- React-Core/RCTWebSocket (0.61.3):
- Folly (= 2018.10.22.00)
- glog
- React-Core/Default (= 0.61.1)
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsiexecutor (= 0.61.1)
- React-Core/Default (= 0.61.3)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsiexecutor (= 0.61.3)
- Yoga
- React-CoreModules (0.61.1):
- FBReactNativeSpec (= 0.61.1)
- React-CoreModules (0.61.3):
- FBReactNativeSpec (= 0.61.3)
- Folly (= 2018.10.22.00)
- RCTTypeSafety (= 0.61.1)
- React-Core/CoreModulesHeaders (= 0.61.1)
- React-RCTImage (= 0.61.1)
- ReactCommon/turbomodule/core (= 0.61.1)
- React-cxxreact (0.61.1):
- RCTTypeSafety (= 0.61.3)
- React-Core/CoreModulesHeaders (= 0.61.3)
- React-RCTImage (= 0.61.3)
- ReactCommon/turbomodule/core (= 0.61.3)
- React-cxxreact (0.61.3):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React-jsinspector (= 0.61.1)
- React-jsi (0.61.1):
- React-jsinspector (= 0.61.3)
- React-jsi (0.61.3):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React-jsi/Default (= 0.61.1)
- React-jsi/Default (0.61.1):
- React-jsi/Default (= 0.61.3)
- React-jsi/Default (0.61.3):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React-jsiexecutor (0.61.1):
- React-jsiexecutor (0.61.3):
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- React-jsinspector (0.61.1)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- React-jsinspector (0.61.3)
- react-native-background-timer (2.1.1):
- React
- react-native-calendar-events (1.7.3):
@@ -278,60 +278,60 @@ PODS:
- React
- react-native-webview (7.4.1):
- React
- React-RCTActionSheet (0.61.1):
- React-Core/RCTActionSheetHeaders (= 0.61.1)
- React-RCTAnimation (0.61.1):
- React-Core/RCTAnimationHeaders (= 0.61.1)
- React-RCTBlob (0.61.1):
- React-Core/RCTBlobHeaders (= 0.61.1)
- React-Core/RCTWebSocket (= 0.61.1)
- React-jsi (= 0.61.1)
- React-RCTNetwork (= 0.61.1)
- React-RCTImage (0.61.1):
- React-Core/RCTImageHeaders (= 0.61.1)
- React-RCTNetwork (= 0.61.1)
- React-RCTLinking (0.61.1):
- React-Core/RCTLinkingHeaders (= 0.61.1)
- React-RCTNetwork (0.61.1):
- React-Core/RCTNetworkHeaders (= 0.61.1)
- React-RCTSettings (0.61.1):
- React-Core/RCTSettingsHeaders (= 0.61.1)
- React-RCTText (0.61.1):
- React-Core/RCTTextHeaders (= 0.61.1)
- React-RCTVibration (0.61.1):
- React-Core/RCTVibrationHeaders (= 0.61.1)
- ReactCommon/jscallinvoker (0.61.1):
- React-RCTActionSheet (0.61.3):
- React-Core/RCTActionSheetHeaders (= 0.61.3)
- React-RCTAnimation (0.61.3):
- React-Core/RCTAnimationHeaders (= 0.61.3)
- React-RCTBlob (0.61.3):
- React-Core/RCTBlobHeaders (= 0.61.3)
- React-Core/RCTWebSocket (= 0.61.3)
- React-jsi (= 0.61.3)
- React-RCTNetwork (= 0.61.3)
- React-RCTImage (0.61.3):
- React-Core/RCTImageHeaders (= 0.61.3)
- React-RCTNetwork (= 0.61.3)
- React-RCTLinking (0.61.3):
- React-Core/RCTLinkingHeaders (= 0.61.3)
- React-RCTNetwork (0.61.3):
- React-Core/RCTNetworkHeaders (= 0.61.3)
- React-RCTSettings (0.61.3):
- React-Core/RCTSettingsHeaders (= 0.61.3)
- React-RCTText (0.61.3):
- React-Core/RCTTextHeaders (= 0.61.3)
- React-RCTVibration (0.61.3):
- React-Core/RCTVibrationHeaders (= 0.61.3)
- ReactCommon/jscallinvoker (0.61.3):
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React-cxxreact (= 0.61.1)
- ReactCommon/turbomodule (0.61.1):
- React-cxxreact (= 0.61.3)
- ReactCommon/turbomodule (0.61.3):
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React-Core (= 0.61.1)
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- ReactCommon/jscallinvoker (= 0.61.1)
- ReactCommon/turbomodule/core (= 0.61.1)
- ReactCommon/turbomodule/samples (= 0.61.1)
- ReactCommon/turbomodule/core (0.61.1):
- React-Core (= 0.61.3)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- ReactCommon/jscallinvoker (= 0.61.3)
- ReactCommon/turbomodule/core (= 0.61.3)
- ReactCommon/turbomodule/samples (= 0.61.3)
- ReactCommon/turbomodule/core (0.61.3):
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React-Core (= 0.61.1)
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- ReactCommon/jscallinvoker (= 0.61.1)
- ReactCommon/turbomodule/samples (0.61.1):
- React-Core (= 0.61.3)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- ReactCommon/jscallinvoker (= 0.61.3)
- ReactCommon/turbomodule/samples (0.61.3):
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React-Core (= 0.61.1)
- React-cxxreact (= 0.61.1)
- React-jsi (= 0.61.1)
- ReactCommon/jscallinvoker (= 0.61.1)
- ReactCommon/turbomodule/core (= 0.61.1)
- React-Core (= 0.61.3)
- React-cxxreact (= 0.61.3)
- React-jsi (= 0.61.3)
- ReactCommon/jscallinvoker (= 0.61.3)
- ReactCommon/turbomodule/core (= 0.61.3)
- RNCAsyncStorage (1.3.4):
- React
- RNGoogleSignin (3.0.1):
@@ -504,8 +504,8 @@ SPEC CHECKSUMS:
Crashlytics: 07fb167b1694128c1c9a5a5cc319b0e9c3ca0933
DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
Fabric: f988e33c97f08930a413e08123064d2e5f68d655
FBLazyVector: 0846affdb2924b01093eb696766ecb0104e409e0
FBReactNativeSpec: c4cf958af1b97799b524f63a26a1c509c0295b04
FBLazyVector: 5bc5b1606fc9a7ac6956de049f6e30901ed31c49
FBReactNativeSpec: f7be9bcc5ce259f7c39509f3f4caf59020d11d4c
Firebase: 02f3281965c075426141a0ce1277e9de6649cab9
FirebaseAnalytics: 23851fe602c872130a2c5c55040b302120346cc2
FirebaseAnalyticsInterop: efbe45c8385ec626e29f9525e5ebd38520dfb6c1
@@ -521,38 +521,38 @@ SPEC CHECKSUMS:
GTMSessionFetcher: 61bb0f61a4cb560030f1222021178008a5727a23
nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48
ObjectiveDropboxOfficial: a5afefc83f6467c42c45f2253f583f2ad1ffc701
RCTRequired: 53825815218847d3e9c7b6d92ad2d197a926d51e
RCTTypeSafety: d886540c518e53064dfa081bf7693fd650699b92
React: 5dea58967c421bd1fdf6b94c18b9ed0f5134683c
React-Core: b381e65aa0da9b94b9dcdc4a99298075b1c3876c
React-CoreModules: 4ed224e29848ba76d26aacb8e3fe85712d3c4fe1
React-cxxreact: 52c98f5c1fb4e4d9f4b588742718350a55f4f088
React-jsi: 61ff417c95e6c3af50fb96399037e80752fb5ce7
React-jsiexecutor: ee45274419eb95614bbbadb98e20684c5f29996e
React-jsinspector: 574d597112f9ea3d1b717f6fb62aef764c70dd6f
RCTRequired: a72523286ea3381f97b28d87529c265baad3ad7d
RCTTypeSafety: e3cc0537400222250f0be37bd69f4b339d3c0a0f
React: 3dc877fc32548b0c7108ca7f301466f4956cbff8
React-Core: ca94e2e7d22cdcc266a405c4d2ad5e5675145776
React-CoreModules: aa415458b5d7dacd10ac1b324d679f6e17cd8685
React-cxxreact: bac5da3d62ee98abd3c1bf7338a7cc6205da7f69
React-jsi: 8bcf5836caa8a759c135ab9ef97f3e023a7b94af
React-jsiexecutor: ae078e9df9c65bcdcf68f9a17656657932d95528
React-jsinspector: a8939cc6909607eb5e8a5ecfff7c6226984e174d
react-native-background-timer: 0d34748e53a972507c66963490c775321a88f6f2
react-native-calendar-events: 2fe35a9294af05de0ed819d3a1b5dac048d2c010
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-webrtc: c5e3d631179a933548a8e49bddbd8fad02586095
react-native-webview: 4dbc1d2a4a6b9c5e9e723c62651917aa2b5e579e
React-RCTActionSheet: af4d951113b1e068bb30611f91b984a7a73597ff
React-RCTAnimation: 4f518d70bb6890b7c3d9d732f84786d6693ca297
React-RCTBlob: 072a4888c08de0eef6d04eaa727d25e577e6ff26
React-RCTImage: 78c5cdf1b2de6cd3cd650dd741868fad19a35528
React-RCTLinking: 486ed1c9a659c7f9fea213868f8930b9a0a79f07
React-RCTNetwork: e79599f3160b459da03447e32b8bcca1a0f0f797
React-RCTSettings: 48b7c5a64ffe0c54c39d59eb7d9036e72305f95a
React-RCTText: 81b62b4e7f11531a5154e4daa5617670d5a2d5de
React-RCTVibration: 8be61459e3749d1fb02cf414edd05b3007622882
ReactCommon: 4fba5be89efdf0b5720e0adb3d8d7edf6e532db0
React-RCTActionSheet: 94671eef55b01a93be735605822ef712d5ea208e
React-RCTAnimation: 524ae33e73de9c0fe6501a7a4bda8e01d26499d9
React-RCTBlob: 5481c2db702f57207af7e7a9b32d90524b821b72
React-RCTImage: b472cc0606f8a7c1ac270d6ccc57123a09439a32
React-RCTLinking: 9cfc7bfdfda078489736695ac476de1f265b9f82
React-RCTNetwork: 967547e4eeac92e55d41573a82da7fff4003052a
React-RCTSettings: 6ab7911172056b5077dacd9240f057eeeb1b121b
React-RCTText: b8f895b94aa0e7778fef28d13f3d71eed4a10c3d
React-RCTVibration: 262588c97551b0b1c675468cda857466ba5af18f
ReactCommon: c2c63d9290b422ca6ad5b3663073a015dd892ae9
RNCAsyncStorage: 8e31405a9f12fbf42c2bb330e4560bfd79c18323
RNGoogleSignin: 39336070b35fc4cea6a98cf111e00480317be0ae
RNSound: c980916b596cc15c8dcd2f6ecd3b13c4881dbe20
RNSVG: aac12785382e8fd4f28d072fe640612e34914631
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
Yoga: d8c572ddec8d05b7dba08e4e5f1924004a177078
Yoga: 02036f6383c0008edb7ef0773a0e6beb6ce82bd1
PODFILE CHECKSUM: cb84b325b724c6ef7c8b24aa52ca7b6f681a095c
PODFILE CHECKSUM: 63c90b1d33cd96709fb72bad6be440ae9c3deecb
COCOAPODS: 1.8.1

103
ios/ci/build-ipa.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/bin/bash
set -e
# Mandatory arguments with no default values provided:
# PR_REPO_SLUG - the Github name of the repo to be merged into the origin/master
# PR_BRANCH - the branch to be merged, if set to "master" no merge will happen
# IPA_DEPLOY_LOCATION - the location understandable by the "scp" command
# executed at the end of the script to deploy the output .ipa file
# LIB_JITSI_MEET_PKG (optional) - the npm package for lib-jitsi-meet which will
# be put in place of the current version in the package.json file.
#
# Other than that the script requires the following env variables to be set:
#
# DEPLOY_SSH_CERT_URL - the SSH private key used by the 'scp' command to deploy
# the .ipa. It is expected to be encrypted with the $ENCRYPTION_PASSWORD.
# ENCRYPTION_PASSWORD - the password used to decrypt certificate/key files used
# in the script.
# IOS_TEAM_ID - the team ID inserted into build-ipa-.plist.template file in
# place of "YOUR_TEAM_ID".
function echoAndExit1() {
echo $1
exit 1
}
if [ -z $PR_REPO_SLUG ]; then
echoAndExit1 "No PR_REPO_SLUG defined"
fi
if [ -z $PR_BRANCH ]; then
echoAndExit1 "No PR_BRANCH defined"
fi
if [ -z $IPA_DEPLOY_LOCATION ]; then
echoAndExit1 "No IPA_DEPLOY_LOCATION defined"
fi
echo "PR_REPO_SLUG=${PR_REPO_SLUG} PR_BRANCH=${PR_BRANCH}"
# do the marge and git log
if [ $PR_BRANCH != "master" ]; then
echo "Will merge ${PR_REPO_SLUG}/${PR_BRANCH} into master"
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch origin master
git checkout master
git pull https://github.com/${PR_REPO_SLUG}.git $PR_BRANCH --no-edit
fi
# Link this lib-jitsi-meet checkout in jitsi-meet through the package.json
if [ ! -z ${LIB_JITSI_MEET_PKG} ];
then
echo "Adjusting lib-jitsi-meet package in package.json to ${LIB_JITSI_MEET_PKG}"
# escape for the sed
LIB_JITSI_MEET_PKG=$(echo $LIB_JITSI_MEET_PKG | sed -e 's/\\/\\\\/g; s/\//\\\//g; s/&/\\\&/g')
sed -i.bak -e "s/\"lib-jitsi-meet.*/\"lib-jitsi-meet\"\: \"${LIB_JITSI_MEET_PKG}\",/g" package.json
echo "Package.json lib-jitsi-meet line:"
grep lib-jitsi-meet package.json
else
echo "LIB_JITSI_MEET_PKG var not set - will not modify the package.json"
fi
git log -20 --graph --pretty=format':%C(yellow)%h%Cblue%d%Creset %s %C(white) %an, %ar%Creset'
# certificates
CERT_DIR="ios/ci/certs"
mkdir -p $CERT_DIR
curl -L -o ${CERT_DIR}/id_rsa.enc ${DEPLOY_SSH_CERT_URL}
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
ssh-add ${CERT_DIR}/id_rsa
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 install --repo-update --no-ansi
cd ..
mkdir -p /tmp/jitsi-meet/
xcodebuild archive -quiet -workspace ios/jitsi-meet.xcworkspace -scheme jitsi-meet -configuration Release -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive
sed -e "s/YOUR_TEAM_ID/${IOS_TEAM_ID}/g" ios/ci/build-ipa.plist.template > ios/ci/build-ipa.plist
IPA_EXPORT_DIR=/tmp/jitsi-meet/jitsi-meet-ipa
xcodebuild -quiet -exportArchive -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive -exportPath $IPA_EXPORT_DIR -exportOptionsPlist ios/ci/build-ipa.plist
echo "Will try deploy the .ipa to: ${IPA_DEPLOY_LOCATION}"
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
rm -r /tmp/jitsi-meet/
rm -r $CERT_DIR

100
ios/ci/setup-certificates.sh Executable file
View File

@@ -0,0 +1,100 @@
# The script is based on tutorial written by Antonis Tsakiridis published at:
# https://medium.com/@atsakiridis/continuous-deployment-for-ios-using-travis-ci-55dcea342d9
#
# APPLE_CERT_URL - the URL pointing to Apple certificate (set to
# http://developer.apple.com/certificationauthority/AppleWWDRCA.cer by default)
# DEPLOY_SSH_CERT_URL - the SSH private key used by the 'scp' command to deploy
# the .ipa. It is expected to be encrypted with the $ENCRYPTION_PASSWORD.
# ENCRYPTION_PASSWORD - the password used to decrypt certificate/key files used
# in the script.
# IOS_DEV_CERT_KEY_URL - URL pointing to provisioning profile certificate key
# file (development-key.p12.enc from the tutorial) encrypted with the
# $ENCRYPTION_PASSWORD.
# IOS_DEV_CERT_URL - URL pointing to provisioning profile certificate file
# (development-cert.cer.enc from the tutorial) encrypted with the
# $ENCRYPTION_PASSWORD.
# IOS_DEV_PROV_PROFILE_URL - URL pointing to provisioning profile file
# (profile-development-olympus.mobileprovision.enc from the tutorial) encrypted
# IOS_DEV_WATCH_PROV_PROFILE_URL - URL pointing to watch app provisioning profile file(encrypted).
# with the $ENCRYPTION_PASSWORD.
# IOS_SIGNING_CERT_PASSWORD - the password to the provisioning profile
# certificate key (used to open development-key.p12 from the tutorial).
function echoAndExit1() {
echo $1
exit 1
}
CERT_DIR=$1
if [ -z $CERT_DIR ]; then
echoAndExit1 "First argument must be certificates directory"
fi
if [ -z $APPLE_CERT_URL ]; then
APPLE_CERT_URL="http://developer.apple.com/certificationauthority/AppleWWDRCA.cer"
fi
if [ -z $DEPLOY_SSH_CERT_URL ]; then
echoAndExit1 "DEPLOY_SSH_CERT_URL env var is not defined"
fi
if [ -z $ENCRYPTION_PASSWORD ]; then
echoAndExit1 "ENCRYPTION_PASSWORD env var is not defined"
fi
if [ -z $IOS_DEV_CERT_KEY_URL ]; then
echoAndExit1 "IOS_DEV_CERT_KEY_URL env var is not defined"
fi
if [ -z $IOS_DEV_CERT_URL ]; then
echoAndExit1 "IOS_DEV_CERT_URL env var is not defined"
fi
if [ -z $IOS_DEV_PROV_PROFILE_URL ]; then
echoAndExit1 "IOS_DEV_PROV_PROFILE_URL env var is not defined"
fi
if [ -z $IOS_DEV_WATCH_PROV_PROFILE_URL ]; then
echoAndExit1 "IOS_DEV_WATCH_PROV_PROFILE_URL env var is not defined"
fi
if [ -z $IOS_SIGNING_CERT_PASSWORD ]; then
echoAndExit1 "IOS_SIGNING_CERT_PASSWORD env var is not defined"
fi
# certificates
curl -L -o ${CERT_DIR}/AppleWWDRCA.cer 'http://developer.apple.com/certificationauthority/AppleWWDRCA.cer'
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}
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
security create-keychain -p $ENCRYPTION_PASSWORD ios-build.keychain
security default-keychain -s ios-build.keychain
security unlock-keychain -p $ENCRYPTION_PASSWORD ios-build.keychain
security set-keychain-settings -t 3600 -l ~/Library/Keychains/ios-build.keychain
echo "importing Apple cert"
security import ${CERT_DIR}/AppleWWDRCA.cer -k ios-build.keychain -A
echo "importing dev-cert.cer"
security import ${CERT_DIR}/dev-cert.cer -k ios-build.keychain -A
echo "importing dev-key.p12"
security import ${CERT_DIR}/dev-key.p12 -k ios-build.keychain -P $IOS_SIGNING_CERT_PASSWORD -A
echo "will set-key-partition-list"
# Fix for OS X Sierra that hungs in the codesign step
security set-key-partition-list -S apple-tool:,apple: -s -k $ENCRYPTION_PASSWORD ios-build.keychain > /dev/null
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/

View File

@@ -2,7 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Original</string>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -627,6 +627,7 @@
baseConfigurationReference = 98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
@@ -658,6 +659,7 @@
baseConfigurationReference = 9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";

View File

@@ -29,8 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -51,8 +49,6 @@
ReferencedContainer = "container:sdk.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -81,7 +77,7 @@
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "exec &gt; /tmp/${PROJECT_NAME}_archive.log 2&gt;&amp;1&#10;&#10;UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal&#10;&#10;if [ &quot;true&quot; == ${ALREADYINVOKED:-false} ]&#10;then&#10;echo &quot;RECURSION: Detected, stopping&quot;&#10;else&#10;export ALREADYINVOKED=&quot;true&quot;&#10;&#10;# make sure the output directory exists&#10;mkdir -p &quot;${UNIVERSAL_OUTPUTFOLDER}&quot;&#10;&#10;echo &quot;Building for iPhoneSimulator&quot;&#10;xcodebuild -workspace &quot;${WORKSPACE_PATH}&quot; -scheme &quot;${TARGET_NAME}&quot; -configuration ${CONFIGURATION} -sdk iphonesimulator -destination &apos;platform=iOS Simulator,name=iPhone 8&apos; ONLY_ACTIVE_ARCH=NO ARCHS=&apos;i386 x86_64&apos; BUILD_DIR=&quot;${BUILD_DIR}&quot; BUILD_ROOT=&quot;${BUILD_ROOT}&quot; ENABLE_BITCODE=YES OTHER_CFLAGS=&quot;-fembed-bitcode&quot; BITCODE_GENERATION_MODE=bitcode clean build&#10;&#10;# Step 1. Copy the framework structure (from iphoneos build) to the universal folder&#10;echo &quot;Copying to output folder&quot;&#10;cp -R &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FULL_PRODUCT_NAME}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/&quot;&#10;&#10;# Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory&#10;SIMULATOR_SWIFT_MODULES_DIR=&quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/.&quot;&#10;if [ -d &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; ]; then&#10;cp -R &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule&quot;&#10;fi&#10;&#10;# Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory&#10;echo &quot;Combining executables&quot;&#10;lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${EXECUTABLE_PATH}&quot;&#10;&#10;# Step 4. Create universal binaries for embedded frameworks&#10;#for SUB_FRAMEWORK in $( ls &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks&quot; ); do&#10;#BINARY_NAME=&quot;${SUB_FRAMEWORK%.*}&quot;&#10;#lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot;&#10;#done&#10;&#10;# Step 5. Convenience step to copy the framework to the project&apos;s directory&#10;echo &quot;Copying to project dir&quot;&#10;yes | cp -Rf &quot;${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}&quot; &quot;${PROJECT_DIR}&quot;&#10;&#10;fi&#10;">
scriptText = "exec &gt; /tmp/${PROJECT_NAME}_archive.log 2&gt;&amp;1&#10;&#10;UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal&#10;&#10;if [ &quot;true&quot; == ${ALREADYINVOKED:-false} ]&#10;then&#10;echo &quot;RECURSION: Detected, stopping&quot;&#10;else&#10;export ALREADYINVOKED=&quot;true&quot;&#10;&#10;# make sure the output directory exists&#10;mkdir -p &quot;${UNIVERSAL_OUTPUTFOLDER}&quot;&#10;&#10;echo &quot;Building for iPhoneSimulator&quot;&#10;xcodebuild -workspace &quot;${WORKSPACE_PATH}&quot; -scheme &quot;${TARGET_NAME}&quot; -configuration ${CONFIGURATION} -sdk iphonesimulator -destination &apos;platform=iOS Simulator,name=iPhone 8&apos; ONLY_ACTIVE_ARCH=NO ARCHS=&apos;i386 x86_64&apos; BUILD_DIR=&quot;${BUILD_DIR}&quot; BUILD_ROOT=&quot;${BUILD_ROOT}&quot; ENABLE_BITCODE=YES OTHER_CFLAGS=&quot;-fembed-bitcode&quot; BITCODE_GENERATION_MODE=bitcode build&#10;&#10;# Step 1. Copy the framework structure (from iphoneos build) to the universal folder&#10;echo &quot;Copying to output folder&quot;&#10;cp -R &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FULL_PRODUCT_NAME}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/&quot;&#10;&#10;# Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory&#10;SIMULATOR_SWIFT_MODULES_DIR=&quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/.&quot;&#10;if [ -d &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; ]; then&#10;cp -R &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule&quot;&#10;fi&#10;&#10;# Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory&#10;echo &quot;Combining executables&quot;&#10;lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${EXECUTABLE_PATH}&quot;&#10;&#10;# Step 4. Create universal binaries for embedded frameworks&#10;#for SUB_FRAMEWORK in $( ls &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks&quot; ); do&#10;#BINARY_NAME=&quot;${SUB_FRAMEWORK%.*}&quot;&#10;#lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot;&#10;#done&#10;&#10;# Step 5. Convenience step to copy the framework to the project&apos;s directory&#10;echo &quot;Copying to project dir&quot;&#10;yes | cp -Rf &quot;${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}&quot; &quot;${PROJECT_DIR}&quot;&#10;&#10;fi&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"

View File

@@ -24,6 +24,7 @@
#import "ReactUtils.h"
#import <RNGoogleSignin/RNGoogleSignin.h>
#import <WebRTC/RTCLogging.h>
@implementation JitsiMeet {
@@ -54,6 +55,11 @@
// Register a log handler for React.
registerReactLogHandler();
#if 0
// Enable WebRTC logs
RTCSetMinDebugLogLevel(RTCLoggingSeverityInfo);
#endif
}
return self;

View File

@@ -37,6 +37,7 @@ set -e
# IOS_TEAM_ID - the team ID inserted into build-ipa-.plist.template file in
# place of "YOUR_TEAM_ID".
# Travis will not print the last echo if there's no sleep 1
function echoSleepAndExit1() {
echo $1
@@ -57,10 +58,6 @@ if [ -z $IPA_DEPLOY_LOCATION ]; then
echoSleepAndExit1 "No IPA_DEPLOY_LOCATION defined"
fi
if [ -z $APPLE_CERT_URL ]; then
APPLE_CERT_URL="http://developer.apple.com/certificationauthority/AppleWWDRCA.cer"
fi
echo "PR_REPO_SLUG=${PR_REPO_SLUG} PR_BRANCH=${PR_BRANCH}"
# do the marge and git log
@@ -88,47 +85,17 @@ fi
git log -20 --graph --pretty=format':%C(yellow)%h%Cblue%d%Creset %s %C(white) %an, %ar%Creset'
# certificates
#certificates
CERT_DIR="ios/travis-ci/certs"
mkdir $CERT_DIR
mkdir -p $CERT_DIR
./ios/ci/setup-certificates.sh $CERT_DIR
curl -L -o ${CERT_DIR}/AppleWWDRCA.cer 'http://developer.apple.com/certificationauthority/AppleWWDRCA.cer'
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
security create-keychain -p $ENCRYPTION_PASSWORD ios-build.keychain
security default-keychain -s ios-build.keychain
security unlock-keychain -p $ENCRYPTION_PASSWORD ios-build.keychain
security set-keychain-settings -t 3600 -l ~/Library/Keychains/ios-build.keychain
echo "importing Apple cert"
security import ${CERT_DIR}/AppleWWDRCA.cer -k ios-build.keychain -A
echo "importing dev-cert.cer"
security import ${CERT_DIR}/dev-cert.cer -k ios-build.keychain -A
echo "importing dev-key.p12"
security import ${CERT_DIR}/dev-key.p12 -k ios-build.keychain -P $IOS_SIGNING_CERT_PASSWORD -A
echo "will set-key-partition-list"
# Fix for OS X Sierra that hungs in the codesign step
security set-key-partition-list -S apple-tool:,apple: -s -k $ENCRYPTION_PASSWORD ios-build.keychain > /dev/null
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/
ssh-add ${CERT_DIR}/id_rsa
npm install
@@ -136,24 +103,21 @@ npm install
./node_modules/react-native-webrtc/tools/downloadBitcode.sh
cd ios
pod update
pod install
pod install --repo-update --no-ansi
cd ..
mkdir -p /tmp/jitsi-meet/
xcodebuild archive -quiet -workspace ios/jitsi-meet.xcworkspace -scheme jitsi-meet -configuration Release -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive
sed -e "s/YOUR_TEAM_ID/${IOS_TEAM_ID}/g" ios/travis-ci/build-ipa.plist.template > ios/travis-ci/build-ipa.plist
sed -e "s/YOUR_TEAM_ID/${IOS_TEAM_ID}/g" ios/ci/build-ipa.plist.template > ios/ci/build-ipa.plist
IPA_EXPORT_DIR=/tmp/jitsi-meet/jitsi-meet-ipa
xcodebuild -quiet -exportArchive -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive -exportPath $IPA_EXPORT_DIR -exportOptionsPlist ios/travis-ci/build-ipa.plist
xcodebuild -quiet -exportArchive -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive -exportPath $IPA_EXPORT_DIR -exportOptionsPlist ios/ci/build-ipa.plist
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}"

View File

@@ -526,16 +526,20 @@
"title": "Settings"
},
"settingsView": {
"advanced": "Advanced",
"alertOk": "OK",
"alertTitle": "Warning",
"alertURLText": "The entered server URL is invalid",
"buildInfoSection": "Build Information",
"conferenceSection": "Conference",
"disableCallIntegration": "Disable native call integration",
"disableP2P": "Disable Peer-To-Peer mode",
"displayName": "Display name",
"email": "Email",
"header": "Settings",
"profileSection": "Profile",
"serverURL": "Server URL",
"showAdvanced": "Show advanced settings",
"startWithAudioMuted": "Start with audio muted",
"startWithVideoMuted": "Start with video muted",
"version": "Version"
@@ -570,6 +574,7 @@
"cc": "Toggle subtitles",
"chat": "Toggle chat window",
"document": "Toggle shared document",
"download": "Download our apps",
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
"hangup": "Leave the call",
@@ -609,6 +614,7 @@
"closeChat": "Close chat",
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"download": "Download our apps",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
"exitFullScreen": "Exit full screen",
@@ -725,8 +731,9 @@
"connectCalendarButton": "Connect your calendar",
"connectCalendarText": "Connect your calendar to view all your meetings in {{app}}. Plus, add {{provider}} meetings to your calendar and start them with one click.",
"enterRoomTitle": "Start a new meeting",
"onlyAsciiAllowed": "Meeting name should only contain latin characters and numbers.",
"roomNameAllowedChars": "Meeting name should not contain any of these characters: ?, &, :, ', \", %, #.",
"go": "GO",
"goSmall": "GO",
"join": "JOIN",
"info": "Info",
"privacy": "Privacy",

4227
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,7 +49,7 @@
"i18next-browser-languagedetector": "3.0.1",
"i18next-xhr-backend": "3.0.0",
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#a885cc98688ef2c3972284bda901596a26ffee52",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
"jquery": "3.4.0",
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.1",
@@ -57,7 +57,7 @@
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f9808adb8eb523bae3318f9f8ef49b544651485f",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#1de69abe22aa632c9a4255ee9f6ae48dab9be756",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",
@@ -68,7 +68,7 @@
"react-emoji-render": "1.0.0",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.61.1",
"react-native": "0.61.3",
"react-native-background-timer": "2.1.1",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#902e6e92d6bae450a6052f76ba4d02f977ffd8f2",
"react-native-callstats": "3.61.0",
@@ -80,7 +80,7 @@
"react-native-svg-transformer": "0.13.0",
"react-native-swipeout": "2.3.6",
"react-native-watch-connectivity": "0.2.0",
"react-native-webrtc": "github:jitsi/react-native-webrtc#047b019a7ce1ec93ab4a2f6796e997d7a02e8e5d",
"react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#a12a6cdfdefe53d03b388394e4cf10966bd99fca",
"react-native-webview": "7.4.1",
"react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0",
@@ -122,14 +122,15 @@
"imports-loader": "0.7.1",
"jetifier": "1.6.4",
"metro-react-native-babel-preset": "0.56.0",
"node-sass": "4.10.0",
"node-sass": "4.12.0",
"precommit-hook": "3.0.0",
"string-replace-loader": "2.1.1",
"style-loader": "0.19.0",
"webpack": "4.26.1",
"unorm": "1.6.0",
"webpack": "4.27.1",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.1.2",
"webpack-dev-server": "3.1.14"
"webpack-dev-server": "3.8.2"
},
"engines": {
"node": ">=8.0.0",

View File

@@ -6,7 +6,7 @@ import '../../analytics';
import '../../authentication';
import { setColorScheme } from '../../base/color-scheme';
import { DialogContainer } from '../../base/dialog';
import { updateFlags } from '../../base/flags';
import { CALL_INTEGRATION_ENABLED, updateFlags } from '../../base/flags';
import '../../base/jwt';
import { Platform } from '../../base/react';
import {
@@ -100,6 +100,13 @@ export class App extends AbstractApp {
dispatch(setColorScheme(this.props.colorScheme));
dispatch(updateFlags(this.props.flags));
dispatch(updateSettings(this.props.userInfo || {}));
// Update settings with feature-flag.
const callIntegrationEnabled = this.props.flags[CALL_INTEGRATION_ENABLED];
if (typeof callIntegrationEnabled !== 'undefined') {
dispatch(updateSettings({ disableCallIntegration: !callIntegrationEnabled }));
}
});
}

View File

@@ -16,7 +16,7 @@ import {
} from '../../base/dialog';
import { translate } from '../../base/i18n';
import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet';
import { StyleType } from '../../base/styles';
import type { StyleType } from '../../base/styles';
import { authenticateAndUpgradeRole, cancelLogin } from '../actions';
import styles from './styles';

View File

@@ -153,6 +153,8 @@ class ColorSchemeRegistry {
const colorScheme = toState(stateful)['features/base/color-scheme'];
return {
...defaultScheme._defaultTheme,
...colorScheme._defaultTheme,
...defaultScheme[componentName],
...colorScheme[componentName]
}[colorDefinition];

View File

@@ -6,18 +6,26 @@ import { ColorPalette, getRGBAFormat } from '../styles';
* The default color scheme of the application.
*/
export default {
'BottomSheet': {
'_defaultTheme': {
// Generic app theme colors that are used accross the entire app.
// All scheme definitions below inherit these values.
background: 'rgb(255, 255, 255)',
icon: '#1c2025',
label: '#1c2025'
icon: 'rgb(28, 32, 37)',
text: 'rgb(28, 32, 37)'
},
'Chat': {
displayName: 'rgb(94, 109, 121)',
localMsgBackground: 'rgb(215, 230, 249)',
privateMsgBackground: 'rgb(250, 219, 219)',
privateMsgNotice: 'rgb(186, 39, 58)',
remoteMsgBackground: 'rgb(241, 242, 246)',
replyBorder: 'rgb(219, 197, 200)',
replyIcon: 'rgb(94, 109, 121)'
},
'Dialog': {
background: 'rgb(255, 255, 255)',
border: 'rgba(0, 3, 6, 0.6)',
buttonBackground: ColorPalette.blue,
buttonLabel: ColorPalette.white,
icon: '#1c2025',
text: '#1c2025'
buttonLabel: ColorPalette.white
},
'Header': {
background: ColorPalette.blue,
@@ -30,8 +38,7 @@ export default {
background: 'rgb(42, 58, 75)'
},
'LoadConfigOverlay': {
background: 'rgb(249, 249, 249)',
text: 'rgb(28, 32, 37)'
background: 'rgb(249, 249, 249)'
},
'Thumbnail': {
activeParticipantHighlight: 'rgb(81, 214, 170)',

View File

@@ -34,3 +34,17 @@ export const LOAD_CONFIG_ERROR = 'LOAD_CONFIG_ERROR';
* }
*/
export const SET_CONFIG = 'SET_CONFIG';
/**
* The redux action which updates the configuration represented by the feature
* base/config. The configuration is defined and consumed by the library
* lib-jitsi-meet but some of its properties are consumed by the application
* jitsi-meet as well. A merge operation is performed between the existing config
* and the passed object.
*
* {
* type: _UPDATE_CONFIG,
* config: Object
* }
*/
export const _UPDATE_CONFIG = '_UPDATE_CONFIG';

View File

@@ -126,6 +126,7 @@ export default [
'startSilent',
'startScreenSharing',
'startVideoMuted',
'startWithAudioMuted',
'startWithVideoMuted',
'subject',
'testing',

View File

@@ -1,5 +1,6 @@
export * from './actions';
export * from './actionTypes';
export { default as CONFIG_WHITELIST } from './configWhitelist';
export * from './functions';
import './middleware';

View File

@@ -5,7 +5,7 @@ import { addKnownDomains } from '../known-domains';
import { MiddlewareRegistry } from '../redux';
import { parseURIString } from '../util';
import { SET_CONFIG } from './actionTypes';
import { _UPDATE_CONFIG, SET_CONFIG } from './actionTypes';
import { _CONFIG_STORE_PREFIX } from './constants';
/**
@@ -93,11 +93,25 @@ function _appWillMount(store, next, action) {
* @private
* @returns {*} The return value of {@code next(action)}.
*/
function _setConfig({ getState }, next, action) {
function _setConfig({ dispatch, getState }, next, action) {
// The reducer is doing some alterations to the config passed in the action,
// so make sure it's the final state by waiting for the action to be
// reduced.
const result = next(action);
const state = getState();
// Update the config with user defined settings.
const settings = state['features/base/settings'];
const config = {};
if (typeof settings.disableP2P !== 'undefined') {
config.p2p = { enabled: !settings.disableP2P };
}
dispatch({
type: _UPDATE_CONFIG,
config
});
// FIXME On Web we rely on the global 'config' variable which gets altered
// multiple times, before it makes it to the reducer. At some point it may
@@ -105,7 +119,7 @@ function _setConfig({ getState }, next, action) {
// different merge methods being used along the way. The global variable
// must be synchronized with the final state resolved by the reducer.
if (typeof window.config !== 'undefined') {
window.config = getState()['features/base/config'];
window.config = state['features/base/config'];
}
return result;

View File

@@ -5,7 +5,7 @@ import _ from 'lodash';
import Platform from '../react/Platform';
import { equals, ReducerRegistry, set } from '../redux';
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
import { _UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
import { _cleanupConfig } from './functions';
/**
@@ -50,55 +50,58 @@ const INITIAL_RN_STATE = {
// fastest to merely disable them.
disableAudioLevels: true,
disableH264: !RN_ENABLE_H264,
p2p: {
disableH264: !RN_ENABLE_H264,
preferH264: RN_ENABLE_H264
}
};
ReducerRegistry.register(
'features/base/config',
(state = _getInitialState(), action) => {
switch (action.type) {
case CONFIG_WILL_LOAD:
ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
switch (action.type) {
case _UPDATE_CONFIG:
return _updateConfig(state, action);
case CONFIG_WILL_LOAD:
return {
error: undefined,
/**
* The URL of the location associated with/configured by this
* configuration.
*
* @type URL
*/
locationURL: action.locationURL
};
case LOAD_CONFIG_ERROR:
// XXX LOAD_CONFIG_ERROR is one of the settlement execution paths of
// the asynchronous "loadConfig procedure/process" started with
// CONFIG_WILL_LOAD. Due to the asynchronous nature of it, whoever
// is settling the process needs to provide proof that they have
// started it and that the iteration of the process being completed
// now is still of interest to the app.
if (state.locationURL === action.locationURL) {
return {
error: undefined,
/**
* The URL of the location associated with/configured by this
* configuration.
*
* @type URL
*/
locationURL: action.locationURL
* The {@link Error} which prevented the loading of the
* configuration of the associated {@code locationURL}.
*
* @type Error
*/
error: action.error
};
case LOAD_CONFIG_ERROR:
// XXX LOAD_CONFIG_ERROR is one of the settlement execution paths of
// the asynchronous "loadConfig procedure/process" started with
// CONFIG_WILL_LOAD. Due to the asynchronous nature of it, whoever
// is settling the process needs to provide proof that they have
// started it and that the iteration of the process being completed
// now is still of interest to the app.
if (state.locationURL === action.locationURL) {
return {
/**
* The {@link Error} which prevented the loading of the
* configuration of the associated {@code locationURL}.
*
* @type Error
*/
error: action.error
};
}
break;
case SET_CONFIG:
return _setConfig(state, action);
}
break;
return state;
});
case SET_CONFIG:
return _setConfig(state, action);
}
return state;
});
/**
* Gets the initial state of the feature base/config. The mandatory
@@ -120,11 +123,10 @@ function _getInitialState() {
* Reduces a specific Redux action SET_CONFIG of the feature
* base/lib-jitsi-meet.
*
* @param {Object} state - The Redux state of the feature base/lib-jitsi-meet.
* @param {Object} state - The Redux state of the feature base/config.
* @param {Action} action - The Redux action SET_CONFIG to reduce.
* @private
* @returns {Object} The new state of the feature base/lib-jitsi-meet after the
* reduction of the specified action.
* @returns {Object} The new state after the reduction of the specified action.
*/
function _setConfig(state, { config }) {
// The mobile app bundles jitsi-meet and lib-jitsi-meet at build time and
@@ -207,3 +209,19 @@ function _translateLegacyConfig(oldValue: Object) {
return newValue;
}
/**
* Updates the stored configuration with the given extra options.
*
* @param {Object} state - The Redux state of the feature base/config.
* @param {Action} action - The Redux action to reduce.
* @private
* @returns {Object} The new state after the reduction of the specified action.
*/
function _updateConfig(state, { config }) {
const newState = _.merge({}, state, config);
_cleanupConfig(newState);
return equals(state, newState) ? state : newState;
}

View File

@@ -147,7 +147,7 @@ ColorSchemeRegistry.register('BottomSheet', {
* Style for the label in a generic item rendered in the menu.
*/
labelStyle: {
color: schemeColor('label'),
color: schemeColor('text'),
flexShrink: 1,
fontSize: MD_FONT_SIZE,
marginLeft: 32,

View File

@@ -6,12 +6,25 @@
*/
export const CALENDAR_ENABLED = 'calendar.enabled';
/**
* Flag indicating if call integration (CallKit on iOS, ConnectionService on Android)
* should be enabled.
* Default: enabled (true).
*/
export const CALL_INTEGRATION_ENABLED = 'call-integration.enabled';
/**
* Flag indicating if chat should be enabled.
* Default: enabled (true).
*/
export const CHAT_ENABLED = 'chat.enabled';
/**
* Flag indicating if invite functionality should be enabled.
* Default: enabled (true).
*/
export const INVITE_ENABLED = 'invite.enabled';
/**
* Flag indicating if recording should be enabled in iOS.
* Default: disabled (false).

View File

@@ -23,6 +23,11 @@ type Props = {
*/
id?: string,
/**
* Function to invoke on click.
*/
onClick?: Function,
/**
* The size of the icon (if not provided by the style object).
*/
@@ -53,6 +58,7 @@ export default function Icon(props: Props) {
className,
color,
id,
onClick,
size,
src: IconComponent,
style
@@ -69,6 +75,7 @@ export default function Icon(props: Props) {
return (
<Container
className = { `jitsi-icon ${className}` }
onClick = { onClick }
style = { restStyle }>
<IconComponent
fill = { calculatedColor }

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4C4 2.89543 4.89543 2 6 2H14C15.1046 2 16 2.89543 16 4H6V18C4.89543 18 4 17.1046 4 16V4ZM10 8V20H18V8H10ZM10 6H18C19.1046 6 20 6.89543 20 8V20C20 21.1046 19.1046 22 18 22H10C8.89543 22 8 21.1046 8 20V8C8 6.89543 8.89543 6 10 6Z" fill="#5E6D7A"/>
</svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 13.3705V5C11 4.44772 11.4477 4 12 4C12.5523 4 13 4.44772 13 5V13.4667L15.631 10.5433C16.0005 10.1328 16.6328 10.0995 17.0433 10.469C17.4538 10.8384 17.4871 11.4707 17.1176 11.8812L12.1064 17.4492C12.0727 17.4867 12.0139 17.4867 11.9802 17.4492L6.96897 11.8812C6.59951 11.4707 6.63278 10.8384 7.04329 10.469C7.4538 10.0995 8.08609 10.1328 8.45555 10.5433L11 13.3705ZM20 15C20 14.4477 20.4477 14 21 14C21.5523 14 22 14.4477 22 15V21C22 21.5523 21.5523 22 21 22H3C2.96548 22 2.93137 21.9983 2.89776 21.9948C2.3935 21.9436 2 21.5178 2 21V15C2 14.4477 2.44772 14 3 14C3.55228 14 4 14.4477 4 15V20H20V15Z" fill="#A4B8D1"/>
</svg>

After

Width:  |  Height:  |  Size: 773 B

View File

@@ -18,11 +18,13 @@ export { default as IconClose } from './close.svg';
export { default as IconClosedCaption } from './closed_caption.svg';
export { default as IconConnectionActive } from './gsm-bars.svg';
export { default as IconConnectionInactive } from './ninja.svg';
export { default as IconCopy } from './copy.svg';
export { default as IconDeviceBluetooth } from './bluetooth.svg';
export { default as IconDeviceEarpiece } from './phone-talk.svg';
export { default as IconDeviceHeadphone } from './headset.svg';
export { default as IconDeviceSpeaker } from './volume.svg';
export { default as IconDominantSpeaker } from './dominant-speaker.svg';
export { default as IconDownload } from './download.svg';
export { default as IconEventNote } from './event_note.svg';
export { default as IconExitFullScreen } from './exit-full-screen.svg';
export { default as IconFeedback } from './feedback.svg';

View File

@@ -63,13 +63,13 @@ export default class JitsiMeetLogStorage {
for (let i = 0, len = logEntries.length; i < len; i++) {
const logEntry = logEntries[i];
if (typeof logEntry === 'object') {
// Aggregated message
logMessage += `(${logEntry.count}) ${logEntry.text}\n`;
} else {
// Regular message
logMessage += `${logEntry}\n`;
if (logEntry.timestamp) {
logMessage += `${logEntry.timestamp} `;
}
if (logEntry.count > 1) {
logMessage += `(${logEntry.count}) `;
}
logMessage += `${logEntry.text}\n`;
}
logMessage += '"}';

View File

@@ -39,7 +39,8 @@ function buildTransport() {
'warn',
'error'
].reduce((logger, logName) => {
logger[logName] = (...args: Array<string>) => {
logger[logName] = (timestamp: string, ...args: Array<string>) => {
// It ignores the timestamp argument, because LogBridge will add it on the native side anyway
const nargs = args.map(arg => {
if (arg instanceof Error) {
const errorBody = {

View File

@@ -12,7 +12,7 @@ import {
} from '../../media';
import { Container, TintedView } from '../../react';
import { connect } from '../../redux';
import { StyleType } from '../../styles';
import type { StyleType } from '../../styles';
import { TestHint } from '../../testing/components';
import { getTrackByMediaTypeAndParticipant } from '../../tracks';

View File

@@ -45,7 +45,7 @@ export default class Linkify extends Component<Props> {
return (
<ReactLinkify
componentDecorator = { this._componentDecorator }>
<Text>
<Text selectable = { true }>
{ this.props.children }
</Text>
</ReactLinkify>

View File

@@ -1,6 +1,5 @@
// @flow
import { parseURLParams } from '../config';
import { CONFIG_WHITELIST, parseURLParams } from '../config';
import { toState } from '../redux';
import { DEFAULT_SERVER_URL } from './constants';
@@ -55,12 +54,14 @@ export function getPropertyValue(
// urlParams
if (sources.urlParams) {
const urlParams
= parseURLParams(state['features/base/connection'].locationURL);
const value = urlParams[`config.${propertyName}`];
if (CONFIG_WHITELIST.indexOf(propertyName) !== -1) {
const urlParams
= parseURLParams(state['features/base/connection'].locationURL);
const value = urlParams[`config.${propertyName}`];
if (typeof value !== 'undefined') {
return value;
if (typeof value !== 'undefined') {
return value;
}
}
}

View File

@@ -0,0 +1,21 @@
// @flow
import { NativeModules } from 'react-native';
export * from './functions.any';
const { AudioMode } = NativeModules;
/**
* Handles changes to the `disableCallIntegration` setting.
* On Android (where `AudioMode.setUseConnectionService` is defined) we must update
* the native side too, since audio routing works differently.
*
* @param {boolean} disabled - Whether call integration is disabled or not.
* @returns {void}
*/
export function handleCallIntegrationChange(disabled: boolean) {
if (AudioMode.setUseConnectionService) {
AudioMode.setUseConnectionService(!disabled);
}
}

View File

@@ -0,0 +1,13 @@
// @flow
export * from './functions.any';
/**
* Handles changes to the `disableCallIntegration` setting.
* Noop on web.
*
* @param {boolean} disabled - Whether call integration is disabled or not.
* @returns {void}
*/
export function handleCallIntegrationChange(disabled: boolean) { // eslint-disable-line no-unused-vars
}

View File

@@ -1,10 +1,12 @@
// @flow
import { APP_WILL_MOUNT } from '../app';
import { setAudioOnly } from '../audio-only';
import { getLocalParticipant, participantUpdated } from '../participants';
import { MiddlewareRegistry } from '../redux';
import { SETTINGS_UPDATED } from './actionTypes';
import { handleCallIntegrationChange } from './functions';
/**
* The middleware of the feature base/settings. Distributes changes to the state
@@ -18,14 +20,34 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_MOUNT:
_initializeCallIntegration(store);
break;
case SETTINGS_UPDATED:
_maybeHandleCallIntegrationChange(action);
_maybeSetAudioOnly(store, action);
_updateLocalParticipant(store, action);
break;
}
return result;
});
/**
* Initializes the audio device handler based on the `disableCallIntegration` setting.
*
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _initializeCallIntegration({ getState }) {
const { disableCallIntegration } = getState()['features/base/settings'];
if (typeof disableCallIntegration === 'boolean') {
handleCallIntegrationChange(disableCallIntegration);
}
}
/**
* Maps the settings field names to participant names where they don't match.
* Currently there is only one such field, but may be extended in the future.
@@ -43,6 +65,19 @@ function _mapSettingsFieldToParticipant(settingsField) {
return settingsField;
}
/**
* Handles a change in the `disableCallIntegration` setting.
*
* @param {Object} action - The redux action.
* @private
* @returns {void}
*/
function _maybeHandleCallIntegrationChange({ settings: { disableCallIntegration } }) {
if (typeof disableCallIntegration === 'boolean') {
handleCallIntegrationChange(disableCallIntegration);
}
}
/**
* Updates {@code startAudioOnly} flag if it's updated in the settings.
*

View File

@@ -22,6 +22,8 @@ const DEFAULT_STATE = {
avatarID: undefined,
avatarURL: undefined,
cameraDeviceId: undefined,
disableCallIntegration: undefined,
disableP2P: undefined,
displayName: undefined,
email: undefined,
localFlipX: true,

View File

@@ -0,0 +1,14 @@
// @flow
import * as unorm from 'unorm';
/**
* Applies NFKC normalization to the given text.
* NOTE: Here we use the unorm package because the JSC version in React Native for Android crashes.
*
* @param {string} text - The text that needs to be normalized.
* @returns {string} - The normalized text.
*/
export function normalizeNFKC(text: string) {
return unorm.nfkc(text);
}

View File

@@ -0,0 +1,11 @@
// @flow
/**
* Applies NFKC normalization to the given text.
*
* @param {string} text - The text that needs to be normalized.
* @returns {string} - The normalized text.
*/
export function normalizeNFKC(text: string) {
return text.normalize('NFKC');
}

View File

@@ -1,5 +1,7 @@
// @flow
import { normalizeNFKC } from './strings';
/**
* The app linking scheme.
* TODO: This should be read from the manifest files later.
@@ -120,8 +122,8 @@ export function getBackendSafeRoomName(room: ?string): ?string {
// But in this case we're fine goin on...
}
// Normalize the character set
room = room.normalize('NFKC');
// Normalize the character set.
room = normalizeNFKC(room);
// Only decoded and normalized strings can be lowercased properly.
room = room.toLowerCase();

View File

@@ -4,6 +4,8 @@ import { PureComponent } from 'react';
import { getLocalizedDateFormatter } from '../../base/i18n';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../constants';
/**
* Formatter string to display the message timestamp.
*/
@@ -65,7 +67,7 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
_getMessageText() {
const { message } = this.props;
return message.messageType === 'error'
return message.messageType === MESSAGE_TYPE_ERROR
? this.props.t('chat.error', {
error: message.message
})
@@ -81,7 +83,7 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
const { message, t } = this.props;
return t('chat.privateNotice', {
recipient: message.messageType === 'local' ? message.recipient : t('chat.you')
recipient: message.messageType === MESSAGE_TYPE_LOCAL ? message.recipient : t('chat.you')
});
}
}

View File

@@ -6,7 +6,7 @@ import { getParticipantDisplayName } from '../../base/participants';
import { setPrivateMessageRecipient } from '../actions';
type Props = {
export type Props = {
/**
* Function used to translate i18n labels.
@@ -27,7 +27,7 @@ type Props = {
/**
* Abstract class for the {@code MessageRecipient} component.
*/
export default class AbstractMessageRecipient extends PureComponent<Props> {
export default class AbstractMessageRecipient<P: Props> extends PureComponent<P> {
}

View File

@@ -3,15 +3,16 @@
import React from 'react';
import { KeyboardAvoidingView, SafeAreaView } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { HeaderWithNavigation, SlidingView } from '../../../base/react';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import AbstractChat, {
_mapDispatchToProps,
_mapStateToProps,
type Props
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractChat';
import ChatInputBar from './ChatInputBar';
@@ -19,6 +20,14 @@ import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
import styles from './styles';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType
};
/**
* Implements a React native component that renders the chat window (modal) of
* the mobile client.
@@ -41,6 +50,8 @@ class Chat extends AbstractChat<Props> {
* @inheritdoc
*/
render() {
const { _styles } = this.props;
return (
<SlidingView
onHide = { this._onClose }
@@ -52,7 +63,7 @@ class Chat extends AbstractChat<Props> {
<HeaderWithNavigation
headerLabelKey = 'chat.title'
onPressBack = { this._onClose } />
<SafeAreaView style = { styles.backdrop }>
<SafeAreaView style = { _styles.backdrop }>
<MessageContainer messages = { this.props._messages } />
<MessageRecipient />
<ChatInputBar onSend = { this.props._onSendMessage } />
@@ -80,4 +91,17 @@ class Chat extends AbstractChat<Props> {
}
}
/**
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
_styles: ColorSchemeRegistry.get(state, 'Chat')
};
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));

View File

@@ -4,16 +4,28 @@ import React from 'react';
import { Text, View } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { Linkify } from '../../../base/react';
import { connect } from '../../../base/redux';
import { type StyleType } from '../../../base/styles';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import { replaceNonUnicodeEmojis } from '../../functions';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
import AbstractChatMessage, { type Props as AbstractProps } from '../AbstractChatMessage';
import PrivateMessageButton from '../PrivateMessageButton';
import styles from './styles';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType
};
/**
* Renders a single chat message.
*/
@@ -24,55 +36,58 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @inheritdoc
*/
render() {
const { message } = this.props;
const localMessage = message.messageType === 'local';
const { _styles, message } = this.props;
const localMessage = message.messageType === MESSAGE_TYPE_LOCAL;
const { privateMessage } = message;
// Style arrays that need to be updated in various scenarios, such as
// error messages or others.
const detailsWrapperStyle = [
styles.detailsWrapper
];
const textWrapperStyle = [
styles.textWrapper
const messageBubbleStyle = [
styles.messageBubble
];
if (localMessage) {
// This is a message sent by the local participant.
// The wrapper needs to be aligned to the right.
detailsWrapperStyle.push(styles.ownMessageDetailsWrapper);
// The bubble needs to be differently styled.
textWrapperStyle.push(styles.ownTextWrapper);
} else if (message.messageType === 'error') {
// The bubble needs to be differently styled.
textWrapperStyle.push(styles.systemTextWrapper);
// The bubble needs some additional styling
messageBubbleStyle.push(_styles.localMessageBubble);
} else if (message.messageType === MESSAGE_TYPE_ERROR) {
// This is a system message.
// The bubble needs some additional styling
messageBubbleStyle.push(styles.systemMessageBubble);
} else {
// This is a remote message sent by a remote participant.
// The bubble needs some additional styling
messageBubbleStyle.push(_styles.remoteMessageBubble);
}
if (privateMessage) {
messageBubbleStyle.push(_styles.privateMessageBubble);
}
return (
<View style = { styles.messageWrapper } >
{ this._renderAvatar() }
<View style = { detailsWrapperStyle }>
<View style = { styles.replyWrapper }>
<View style = { textWrapperStyle } >
{
this.props.showDisplayName
&& this._renderDisplayName()
}
<View style = { messageBubbleStyle }>
<View style = { styles.textWrapper } >
{ this._renderDisplayName() }
<Linkify linkStyle = { styles.chatLink }>
{ replaceNonUnicodeEmojis(this._getMessageText()) }
</Linkify>
{
message.privateMessage
&& this._renderPrivateNotice()
}
{ this._renderPrivateNotice() }
</View>
{ message.privateMessage && !localMessage
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false }
toggledStyles = { styles.replyStyles } /> }
{ this._renderPrivateReplyButton() }
</View>
{ this.props.showTimestamp && this._renderTimestamp() }
{ this._renderTimestamp() }
</View>
</View>
);
@@ -104,37 +119,77 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
/**
* Renders the display name of the sender.
* Renders the display name of the sender if necessary.
*
* @returns {React$Element<*>}
* @returns {React$Element<*> | null}
*/
_renderDisplayName() {
const { _styles, message, showDisplayName } = this.props;
if (!showDisplayName) {
return null;
}
return (
<Text style = { styles.displayName }>
{ this.props.message.displayName }
<Text style = { _styles.displayName }>
{ message.displayName }
</Text>
);
}
/**
* Renders the message privacy notice.
* Renders the message privacy notice, if necessary.
*
* @returns {React$Element<*>}
* @returns {React$Element<*> | null}
*/
_renderPrivateNotice() {
const { _styles, message } = this.props;
if (!message.privateMessage) {
return null;
}
return (
<Text style = { styles.privateNotice }>
<Text style = { _styles.privateNotice }>
{ this._getPrivateNoticeMessage() }
</Text>
);
}
/**
* Renders the time at which the message was sent.
* Renders the private reply button, if necessary.
*
* @returns {React$Element<*>}
* @returns {React$Element<*> | null}
*/
_renderPrivateReplyButton() {
const { _styles, message } = this.props;
const { messageType, privateMessage } = message;
if (!privateMessage || messageType === MESSAGE_TYPE_LOCAL) {
return null;
}
return (
<View style = { _styles.replyContainer }>
<PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false }
toggledStyles = { _styles.replyStyles } />
</View>
);
}
/**
* Renders the time at which the message was sent, if necessary.
*
* @returns {React$Element<*> | null}
*/
_renderTimestamp() {
if (!this.props.showTimestamp) {
return null;
}
return (
<Text style = { styles.timeText }>
{ this._getFormattedTimestamp() }
@@ -143,4 +198,16 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
}
export default translate(ChatMessage);
/**
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'Chat')
};
}
export default translate(connect(_mapStateToProps)(ChatMessage));

View File

@@ -3,6 +3,8 @@
import React, { Component } from 'react';
import { FlatList } from 'react-native';
import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants';
import ChatMessage from './ChatMessage';
import styles from './styles';
@@ -73,11 +75,11 @@ export default class ChatMessageGroup extends Component<Props> {
<ChatMessage
message = { message }
showAvatar = {
this.props.messages[0].messageType !== 'local'
this.props.messages[0].messageType !== MESSAGE_TYPE_LOCAL
&& index === this.props.messages.length - 1
}
showDisplayName = {
this.props.messages[0].messageType === 'remote'
this.props.messages[0].messageType === MESSAGE_TYPE_REMOTE
&& index === this.props.messages.length - 1
}
showTimestamp = { index === 0 } />

View File

@@ -3,28 +3,37 @@
import React from 'react';
import { Text, TouchableHighlight, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { Icon, IconCancelSelection } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { type StyleType } from '../../../base/styles';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractMessageRecipient';
import styles from './styles';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType
};
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
class MessageRecipient extends AbstractMessageRecipient<Props> {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _privateMessageRecipient } = this.props;
const { _privateMessageRecipient, _styles } = this.props;
if (!_privateMessageRecipient) {
return null;
@@ -33,8 +42,8 @@ class MessageRecipient extends AbstractMessageRecipient {
const { t } = this.props;
return (
<View style = { styles.messageRecipientContainer }>
<Text style = { styles.messageRecipientText }>
<View style = { _styles.messageRecipientContainer }>
<Text style = { _styles.messageRecipientText }>
{ t('chat.messageTo', {
recipient: _privateMessageRecipient
}) }
@@ -42,11 +51,24 @@ class MessageRecipient extends AbstractMessageRecipient {
<TouchableHighlight onPress = { this.props._onRemovePrivateMessageRecipient }>
<Icon
src = { IconCancelSelection }
style = { styles.messageRecipientCancelIcon } />
style = { _styles.messageRecipientCancelIcon } />
</TouchableHighlight>
</View>
);
}
}
/**
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
_styles: ColorSchemeRegistry.get(state, 'Chat')
};
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));

View File

@@ -1,7 +1,10 @@
// @flow
import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
import { BoxModel, ColorPalette } from '../../../base/styles';
const BUBBLE_RADIUS = 8;
/**
* The styles of the feature chat.
*
@@ -20,14 +23,6 @@ export default {
width: 32
},
/**
* Background of the chat screen.
*/
backdrop: {
backgroundColor: ColorPalette.white,
flex: 1
},
chatContainer: {
alignItems: 'stretch',
flex: 1,
@@ -47,14 +42,6 @@ export default {
flexDirection: 'column'
},
/**
* The text node for the display name.
*/
displayName: {
color: 'rgb(118, 136, 152)',
fontSize: 13
},
/**
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
*/
@@ -76,35 +63,16 @@ export default {
height: 48
},
messageBubble: {
alignItems: 'center',
borderRadius: BUBBLE_RADIUS,
flexDirection: 'row'
},
messageContainer: {
flex: 1
},
messageRecipientCancelIcon: {
color: ColorPalette.white,
fontSize: 18
},
messageRecipientContainer: {
alignItems: 'center',
backgroundColor: ColorPalette.warning,
flexDirection: 'row',
padding: BoxModel.padding
},
messageRecipientText: {
color: ColorPalette.white,
flex: 1
},
/**
* The message text itself.
*/
messageText: {
color: 'rgb(28, 32, 37)',
fontSize: 15
},
/**
* Wrapper View for the entire block.
*/
@@ -123,34 +91,11 @@ export default {
alignItems: 'flex-end'
},
/**
* Style modifier for the {@code textWrapper} for own messages.
*/
ownTextWrapper: {
backgroundColor: 'rgb(210, 231, 249)',
borderTopLeftRadius: 8,
borderTopRightRadius: 0
},
replyWrapper: {
alignItems: 'center',
flexDirection: 'row'
},
replyStyles: {
iconStyle: {
color: 'rgb(118, 136, 152)',
fontSize: 22,
margin: BoxModel.margin / 2
}
},
privateNotice: {
color: ColorPalette.warning,
fontSize: 13,
fontStyle: 'italic'
},
sendButtonIcon: {
color: ColorPalette.darkGrey,
fontSize: 22
@@ -159,7 +104,7 @@ export default {
/**
* Style modifier for system (error) messages.
*/
systemTextWrapper: {
systemMessageBubble: {
backgroundColor: 'rgb(247, 215, 215)'
},
@@ -168,9 +113,6 @@ export default {
*/
textWrapper: {
alignItems: 'flex-start',
backgroundColor: 'rgb(240, 243, 247)',
borderRadius: 8,
borderTopLeftRadius: 0,
flexDirection: 'column',
padding: 9
},
@@ -183,3 +125,73 @@ export default {
fontSize: 13
}
};
ColorSchemeRegistry.register('Chat', {
/**
* Background of the chat screen.
*/
backdrop: {
backgroundColor: schemeColor('background'),
flex: 1
},
/**
* The text node for the display name.
*/
displayName: {
color: schemeColor('displayName'),
fontSize: 13
},
localMessageBubble: {
backgroundColor: schemeColor('localMsgBackground'),
borderTopRightRadius: 0
},
messageRecipientCancelIcon: {
color: schemeColor('icon'),
fontSize: 18
},
messageRecipientContainer: {
alignItems: 'center',
backgroundColor: schemeColor('privateMsgBackground'),
flexDirection: 'row',
padding: BoxModel.padding
},
messageRecipientText: {
color: schemeColor('text'),
flex: 1
},
privateNotice: {
color: schemeColor('privateMsgNotice'),
fontSize: 11,
marginTop: 6
},
privateMessageBubble: {
backgroundColor: schemeColor('privateMsgBackground')
},
remoteMessageBubble: {
backgroundColor: schemeColor('remoteMsgBackground'),
borderTopLeftRadius: 0
},
replyContainer: {
alignSelf: 'stretch',
borderLeftColor: schemeColor('replyBorder'),
borderLeftWidth: 1,
justifyContent: 'center'
},
replyStyles: {
iconStyle: {
color: schemeColor('replyIcon'),
fontSize: 22,
padding: 8
}
}
});

View File

@@ -4,6 +4,7 @@ import React from 'react';
import Transition from 'react-transition-group/Transition';
import { translate } from '../../../base/i18n';
import { Icon, IconClose } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractChat, {
@@ -137,7 +138,9 @@ class Chat extends AbstractChat<Props> {
<div className = 'chat-header'>
<div
className = 'chat-close'
onClick = { this.props._onToggleChat }>X</div>
onClick = { this.props._onToggleChat }>
<Icon src = { IconClose } />
</div>
</div>
);
}

View File

@@ -7,6 +7,8 @@ import { toArray } from 'react-emoji-render';
import { translate } from '../../../base/i18n';
import { Linkify } from '../../../base/react';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import AbstractChatMessage, {
type Props
} from '../AbstractChatMessage';
@@ -39,19 +41,25 @@ class ChatMessage extends AbstractChatMessage<Props> {
return (
<div className = 'chatmessage-wrapper'>
<div className = 'replywrapper'>
<div className = 'chatmessage'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
{ processedMessage }
<div className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''}` }>
<div className = 'replywrapper'>
<div className = 'messagecontent'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
{ processedMessage }
</div>
{ message.privateMessage && this._renderPrivateNotice() }
</div>
{ message.privateMessage && this._renderPrivateNotice() }
{ message.privateMessage && message.messageType !== MESSAGE_TYPE_LOCAL
&& (
<div className = 'messageactions'>
<PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false } />
</div>
) }
</div>
{ message.privateMessage && message.messageType !== 'local'
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false } /> }
</div>
{ this.props.showTimestamp && this._renderTimestamp() }
</div>

View File

@@ -2,6 +2,8 @@
import React from 'react';
import { MESSAGE_TYPE_REMOTE } from '../../constants';
import AbstractMessageContainer, { type Props }
from '../AbstractMessageContainer';
@@ -61,7 +63,7 @@ export default class MessageContainer extends AbstractMessageContainer {
return (
<ChatMessageGroup
className = { messageType || 'remote' }
className = { messageType || MESSAGE_TYPE_REMOTE }
key = { index }
messages = { group } />
);

View File

@@ -8,13 +8,14 @@ import { connect } from '../../../base/redux';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
_mapStateToProps,
type Props
} from '../AbstractMessageRecipient';
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
class MessageRecipient extends AbstractMessageRecipient<Props> {
/**
* Implements {@code PureComponent#render}.
*

View File

@@ -1,3 +1,5 @@
// @flow
/**
* The audio ID of the audio element for which the {@link playAudio} action is
* triggered when new chat message is received.
@@ -5,3 +7,18 @@
* @type {string}
*/
export const INCOMING_MSG_SOUND_ID = 'INCOMING_MSG_SOUND';
/**
* The {@code messageType} of error (system) messages.
*/
export const MESSAGE_TYPE_ERROR = 'error';
/**
* The {@code messageType} of local messages.
*/
export const MESSAGE_TYPE_LOCAL = 'local';
/**
* The {@code messageType} of remote messages.
*/
export const MESSAGE_TYPE_REMOTE = 'remote';

View File

@@ -22,7 +22,7 @@ import { isButtonEnabled, showToolbox } from '../toolbox';
import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
import { addMessage, clearMessages, toggleChat } from './actions';
import { ChatPrivacyDialog } from './components';
import { INCOMING_MSG_SOUND_ID } from './constants';
import { INCOMING_MSG_SOUND_ID, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from './constants';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
declare var APP: Object;
@@ -194,7 +194,7 @@ function _addChatMsgListener(conference, store) {
function _handleChatError({ dispatch }, error) {
dispatch(addMessage({
hasRead: true,
messageType: 'error',
messageType: MESSAGE_TYPE_ERROR,
message: error,
privateMessage: false,
timestamp: Date.now()
@@ -231,7 +231,7 @@ function _handleReceivedMessage({ dispatch, getState }, { id, message, nick, pri
displayName,
hasRead,
id,
messageType: participant.local ? 'local' : 'remote',
messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
message,
privateMessage,
recipient: getParticipantDisplayName(state, localParticipant.id),
@@ -285,7 +285,7 @@ function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message
displayName,
hasRead: true,
id: localParticipant.id,
messageType: 'local',
messageType: MESSAGE_TYPE_LOCAL,
message,
privateMessage: true,
recipient: getParticipantDisplayName(getState, recipientID),
@@ -323,7 +323,7 @@ function _shouldSendPrivateMessageTo(state, action): ?string {
const lastMessage = navigator.product === 'ReactNative'
? messages[0] : messages[messages.length - 1];
if (lastMessage.messageType === 'local') {
if (lastMessage.messageType === MESSAGE_TYPE_LOCAL) {
// The sender is probably aware of any private messages as already sent
// a message since then. Doesn't make sense to display the notice now.
return undefined;
@@ -339,7 +339,7 @@ function _shouldSendPrivateMessageTo(state, action): ?string {
const now = Date.now();
const recentPrivateMessages = messages.filter(
message =>
message.messageType !== 'local'
message.messageType !== MESSAGE_TYPE_LOCAL
&& message.privateMessage
&& message.timestamp + PRIVACY_NOTICE_TIMEOUT > now);
const recentPrivateMessage = navigator.product === 'ReactNative'

View File

@@ -0,0 +1,101 @@
// @flow
import React, { PureComponent } from 'react';
import type { Dispatch } from 'redux';
import { openDialog } from '../../../base/dialog';
import { getParticipantCount } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { SpeakerStats } from '../../../speaker-stats';
/**
* The type of the React {@code Component} props of {@link ParticipantsCount}.
*/
type Props = {
/**
* Number of the conference participants.
*/
count: string,
/**
* Conference data.
*/
conference: Object,
/**
* Invoked to open Speaker stats.
*/
dispatch: Dispatch<any>,
};
/**
* ParticipantsCount react component.
* Displays the number of participants and opens Speaker stats on click.
*
* @class ParticipantsCount
*/
class ParticipantsCount extends PureComponent<Props> {
/**
* Initializes a new ParticipantsCount instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this._onClick = this._onClick.bind(this);
}
_onClick: () => void;
/**
* Callback invoked to display {@code SpeakerStats}.
*
* @private
* @returns {void}
*/
_onClick() {
const { dispatch, conference } = this.props;
dispatch(openDialog(SpeakerStats, { conference }));
}
/**
* Implements React's {@link PureComponent#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div
className = 'participants-count'
onClick = { this._onClick }>
<span className = 'participants-count-number'>
{this.props.count}
</span>
<span className = 'participants-count-icon' />
</div>
);
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code ParticipantsCount} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function mapStateToProps(state) {
return {
conference: state['features/base/conference'].conference,
count: getParticipantCount(state)
};
}
export default connect(mapStateToProps)(ParticipantsCount);

View File

@@ -2,16 +2,20 @@
import React, { Component } from 'react';
import { getConferenceName } from '../../../base/conference/functions';
import { connect } from '../../../base/redux';
import { isToolboxVisible } from '../../../toolbox';
import ParticipantsCount from './ParticipantsCount';
/**
* The type of the React {@code Component} props of {@link Subject}.
*/
type Props = {
/**
* The subject of the conference.
* The subject or the of the conference.
* Falls back to conference name.
*/
_subject: string,
@@ -39,7 +43,8 @@ class Subject extends Component<Props> {
return (
<div className = { `subject ${_visible ? 'visible' : ''}` }>
{ _subject }
<span className = 'subject-text'>{ _subject }</span>
<ParticipantsCount />
</div>
);
}
@@ -57,10 +62,9 @@ class Subject extends Component<Props> {
* }}
*/
function _mapStateToProps(state) {
const { subject } = state['features/base/conference'];
return {
_subject: subject,
_subject: getConferenceName(state),
_visible: isToolboxVisible(state)
};
}

View File

@@ -10,6 +10,7 @@ import { DialInSummary } from '../../invite';
import { _TNS } from '../constants';
import { generateDeepLinkingURL } from '../functions';
import { renderPromotionalFooter } from '../renderPromotionalFooter';
declare var interfaceConfig: Object;
@@ -130,6 +131,7 @@ class DeepLinkingMobilePage extends Component<Props> {
{ t(`${_TNS}.openApp`) }
{/* </button> */}
</a>
{ renderPromotionalFooter() }
<DialInSummary
className = 'deep-linking-dial-in'
clickableNumbers = { true }

View File

@@ -0,0 +1,9 @@
// @flow
/**
* Method used in order to render a custom promotional footer.
*
* @returns {HTMLElement}
*/
export function renderPromotionalFooter() {
return null;
}

View File

@@ -1,7 +1,7 @@
// @flow
import React, { PureComponent } from 'react';
import { SafeAreaView, View } from 'react-native';
import { Platform, SafeAreaView, View } from 'react-native';
import { WebView } from 'react-native-webview';
import type { Dispatch } from 'redux';
@@ -71,6 +71,13 @@ class SharedDocument extends PureComponent<Props> {
render() {
const { _documentUrl, _isOpen } = this.props;
const webViewStyles = this._getWebViewStyles();
const extraWebViewProps = {};
// FIXME HACK: workaround nginx spartan config issue on meet.jit.si
if (Platform.OS === 'android') {
// eslint-disable-next-line max-len
extraWebViewProps.userAgent = 'Mozilla/5.0 (Linux; Android 8.0.0;) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.96 Mobile Safari/537.36';
}
return (
<SlidingView
@@ -86,7 +93,8 @@ class SharedDocument extends PureComponent<Props> {
onError = { this._onError }
renderLoading = { this._renderLoading }
source = {{ uri: _documentUrl }}
startInLoadingState = { true } />
startInLoadingState = { true }
{ ...extraWebViewProps } />
</SafeAreaView>
</View>
</SlidingView>

View File

@@ -40,12 +40,12 @@ class Toolbar extends Component<Props> {
<div
className = 'filmstrip-toolbox'
id = 'new-toolbox'>
<AudioMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('microphone') } />
<HangupButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('hangup') } />
<AudioMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('microphone') } />
<VideoMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('camera') } />

View File

@@ -139,7 +139,6 @@ function _onFollowMeCommand(attributes = {}, id, store) {
// For now gate etherpad checks behind a web-app check to be extra safe
// against calling a web-app global.
if (typeof APP !== 'undefined'
&& state['features/etherpad'].initialized
&& oldState.sharedDocumentVisible !== attributes.sharedDocumentVisible) {
const isEtherpadVisible = attributes.sharedDocumentVisible === 'true';
const documentManager = APP.UI.getSharedDocumentManager();

View File

@@ -2,6 +2,7 @@
import type { Dispatch } from 'redux';
import { getFeatureFlag, INVITE_ENABLED } from '../../../../base/flags';
import { translate } from '../../../../base/i18n';
import { IconAddPeople } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
@@ -50,7 +51,8 @@ class InviteButton extends AbstractButton<Props, *> {
* @returns {Object}
*/
function _mapStateToProps(state: Object, ownProps: Object) {
const addPeopleEnabled = isAddPeopleEnabled(state) || isDialOutEnabled(state);
const addPeopleEnabled = getFeatureFlag(state, INVITE_ENABLED, true)
&& (isAddPeopleEnabled(state) || isDialOutEnabled(state));
const { visible = Boolean(addPeopleEnabled) } = ownProps;
return {

View File

@@ -7,7 +7,7 @@ import { setPassword } from '../../../../base/conference';
import { getInviteURL } from '../../../../base/connection';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { Icon, IconInfo } from '../../../../base/icons';
import { Icon, IconInfo, IconCopy } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
import {
isLocalParticipantModerator,
@@ -136,6 +136,7 @@ type State = {
*/
class InfoDialog extends Component<Props, State> {
_copyElement: ?Object;
_copyUrlElement: ?Object;
/**
* Implements React's {@link Component#getDerivedStateFromProps()}.
@@ -197,12 +198,14 @@ class InfoDialog extends Component<Props, State> {
// Bind event handlers so they are only bound once for every instance.
this._onClickURLText = this._onClickURLText.bind(this);
this._onCopyInviteURL = this._onCopyInviteURL.bind(this);
this._onCopyInviteInfo = this._onCopyInviteInfo.bind(this);
this._onCopyInviteUrl = this._onCopyInviteUrl.bind(this);
this._onPasswordRemove = this._onPasswordRemove.bind(this);
this._onPasswordSubmit = this._onPasswordSubmit.bind(this);
this._onTogglePasswordEditState
= this._onTogglePasswordEditState.bind(this);
this._setCopyElement = this._setCopyElement.bind(this);
this._setCopyUrlElement = this._setCopyUrlElement.bind(this);
}
/**
@@ -239,12 +242,18 @@ class InfoDialog extends Component<Props, State> {
<span className = 'spacer'>&nbsp;</span>
<span className = 'info-value'>
<a
className = 'info-dialog-url-text'
className = 'info-dialog-url-text info-dialog-url-text-unselectable'
href = { this.props._inviteURL }
onClick = { this._onClickURLText } >
{ decodeURI(this._getURLToDisplay()) }
</a>
</span>
<span className = 'info-dialog-url-icon'>
<Icon
onClick = { this._onCopyInviteUrl }
size = { 18 }
src = { IconCopy } />
</span>
</div>
<div className = 'info-dialog-dial-in'>
{ this._renderDialInDisplay() }
@@ -262,7 +271,7 @@ class InfoDialog extends Component<Props, State> {
<div className = 'info-dialog-action-link'>
<a
className = 'info-copy'
onClick = { this._onCopyInviteURL }>
onClick = { this._onCopyInviteInfo }>
{ t('dialog.copy') }
</a>
</div>
@@ -275,6 +284,12 @@ class InfoDialog extends Component<Props, State> {
ref = { this._setCopyElement }
tabIndex = '-1'
value = { this._getTextToCopy() } />
<textarea
className = 'info-dialog-copy-element'
readOnly = { true }
ref = { this._setCopyUrlElement }
tabIndex = '-1'
value = { this.props._inviteURL } />
</div>
);
@@ -365,7 +380,7 @@ class InfoDialog extends Component<Props, State> {
event.preventDefault();
}
_onCopyInviteURL: () => void;
_onCopyInviteInfo: () => void;
/**
* Callback invoked to copy the contents of {@code this._copyElement} to the
@@ -374,7 +389,7 @@ class InfoDialog extends Component<Props, State> {
* @private
* @returns {void}
*/
_onCopyInviteURL() {
_onCopyInviteInfo() {
try {
if (!this._copyElement) {
throw new Error('No element to copy from.');
@@ -388,6 +403,28 @@ class InfoDialog extends Component<Props, State> {
}
}
_onCopyInviteUrl: () => void;
/**
* Callback invoked to copy the contents of {@code this._copyUrlElement} to the clipboard.
*
* @private
* @returns {void}
*/
_onCopyInviteUrl() {
try {
if (!this._copyUrlElement) {
throw new Error('No element to copy from.');
}
this._copyUrlElement && this._copyUrlElement.select();
document.execCommand('copy');
this._copyUrlElement && this._copyUrlElement.blur();
} catch (err) {
logger.error('error when copying the text', err);
}
}
_onPasswordRemove: () => void;
/**
@@ -565,6 +602,21 @@ class InfoDialog extends Component<Props, State> {
_setCopyElement(element: Object) {
this._copyElement = element;
}
_setCopyUrlElement: () => void;
/**
* Sets the internal reference to the DOM/HTML element backing the React
* {@code Component} input.
*
* @param {HTMLInputElement} element - The DOM/HTML element for this
* {@code Component}'s input.
* @private
* @returns {void}
*/
_setCopyUrlElement(element: Object) {
this._copyUrlElement = element;
}
}
/**

View File

@@ -577,5 +577,12 @@ export function _decodeRoomURI(url: string) {
roomUrl = decodeURI(roomUrl);
}
// Handles a special case where the room name has % encoded, the decoded will have
// % followed by a char (non-digit) which is not a valid URL and room name ... so we do not
// want to show this decoded
if (roomUrl.match(/.*%[^\d].*/)) {
return url;
}
return roomUrl;
}

View File

@@ -0,0 +1,20 @@
// @flow
import { CALL_INTEGRATION_ENABLED, getFeatureFlag } from '../../base/flags';
import { toState } from '../../base/redux';
/**
* Checks if call integration is enabled or not.
*
* @param {Function|Object} stateful - The redux store or {@code getState}
* function.
* @returns {string} - Default URL for the app.
*/
export function isCallIntegrationEnabled(stateful: Function | Object) {
const state = toState(stateful);
const { disableCallIntegration } = state['features/base/settings'];
const flag = getFeatureFlag(state, CALL_INTEGRATION_ENABLED);
// The feature flag has precedence.
return flag ?? !disableCallIntegration;
}

View File

@@ -34,6 +34,7 @@ import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
import CallKit from './CallKit';
import ConnectionService from './ConnectionService';
import { isCallIntegrationEnabled } from './functions';
const CallIntegration = CallKit || ConnectionService;
@@ -139,9 +140,13 @@ function _appWillMount({ dispatch, getState }, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceFailed(store, next, action) {
function _conferenceFailed({ getState }, next, action) {
const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may be
// able to eventually join the conference.
@@ -173,6 +178,10 @@ function _conferenceFailed(store, next, action) {
function _conferenceJoined({ getState }, next, action) {
const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
const { callUUID } = action.conference;
if (callUUID) {
@@ -201,9 +210,13 @@ function _conferenceJoined({ getState }, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceLeft(store, next, action) {
function _conferenceLeft({ getState }, next, action) {
const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
const { callUUID } = action.conference;
if (callUUID) {
@@ -230,6 +243,10 @@ function _conferenceLeft(store, next, action) {
function _conferenceWillJoin({ dispatch, getState }, next, action) {
const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
const { conference } = action;
const state = getState();
const { callHandle, callUUID } = state['features/base/config'];
@@ -341,6 +358,11 @@ function _onPerformSetMutedCallAction({ callUUID, muted }) {
function _setAudioOnly({ getState }, next, action) {
const result = next(action);
const state = getState();
if (!isCallIntegrationEnabled(state)) {
return result;
}
const conference = getCurrentConference(state);
if (conference && conference.callUUID) {
@@ -393,6 +415,11 @@ function _setCallKitSubscriptions({ getState }, next, action) {
*/
function _syncTrackState({ getState }, next, action) {
const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
const { jitsiTrack } = action.track;
const state = getState();
const conference = getCurrentConference(state);

View File

@@ -1,37 +0,0 @@
/**
* The type of redux action to add a network request to the redux store/state.
*
* {
* type: _ADD_NETWORK_REQUEST,
* request: Object
* }
*
* @protected
*/
export const _ADD_NETWORK_REQUEST = '_ADD_NETWORK_REQUEST';
/**
* The type of redux action to remove all network requests from the redux
* store/state.
*
* {
* type: _REMOVE_ALL_NETWORK_REQUESTS,
* }
*
* @protected
*/
export const _REMOVE_ALL_NETWORK_REQUESTS
= '_REMOVE_ALL_NETWORK_REQUESTS';
/**
* The type of redux action to remove a network request from the redux
* store/state.
*
* {
* type: _REMOVE_NETWORK_REQUEST,
* request: Object
* }
*
* @protected
*/
export const _REMOVE_NETWORK_REQUEST = '_REMOVE_NETWORK_REQUEST';

View File

@@ -1,55 +0,0 @@
// @flow
import React, { Component } from 'react';
import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
/**
* The type of the React {@code Component} props of
* {@link NetworkActivityIndicator}.
*/
type Props = {
/**
* Indicates whether there is network activity i.e. ongoing network
* requests.
*/
_networkActivity: boolean
};
/**
* The React {@code Component} which renders a progress indicator when there
* are ongoing network requests.
*/
class NetworkActivityIndicator extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return this.props._networkActivity ? <LoadingIndicator /> : null;
}
}
/**
* Maps (parts of) the redux state to the React {@code Component} props of
* {@code NetworkActivityIndicator}.
*
* @param {Object} state - The redux state.
* @private
* @returns {{
* _networkActivity: boolean
* }}
*/
function _mapStateToProps(state) {
const { requests } = state['features/network-activity'];
return {
_networkActivity: Boolean(requests && requests.size)
};
}
export default connect(_mapStateToProps)(NetworkActivityIndicator);

View File

@@ -1,3 +0,0 @@
export {
default as NetworkActivityIndicator
} from './NetworkActivityIndicator';

View File

@@ -1,4 +0,0 @@
export * from './components';
import './middleware';
import './reducer';

View File

@@ -1,81 +0,0 @@
/* @flow */
import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { MiddlewareRegistry } from '../../base/redux';
import {
_ADD_NETWORK_REQUEST,
_REMOVE_ALL_NETWORK_REQUESTS,
_REMOVE_NETWORK_REQUEST
} from './actionTypes';
/**
* Middleware which captures app startup and conference actions in order to
* clear the image cache.
*
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_MOUNT:
_startNetInterception(store);
break;
case APP_WILL_UNMOUNT:
_stopNetInterception(store);
break;
}
return result;
});
/**
* Starts intercepting network requests. Only XHR requests are supported at the
* moment.
*
* Ongoing request information is kept in redux, and it's removed as soon as a
* given request is complete.
*
* @param {Object} store - The redux store.
* @private
* @returns {void}
*/
function _startNetInterception({ dispatch }) {
XHRInterceptor.setOpenCallback((method, url, xhr) => dispatch({
type: _ADD_NETWORK_REQUEST,
request: xhr,
// The following are not really necessary read anywhere at the time of
// this writing but are provided anyway if the reducer chooses to
// remember them:
method,
url
}));
XHRInterceptor.setResponseCallback((...args) => dispatch({
type: _REMOVE_NETWORK_REQUEST,
// XXX The XHR is the last argument of the responseCallback.
request: args[args.length - 1]
}));
XHRInterceptor.enableInterception();
}
/**
* Stops intercepting network requests.
*
* @param {Object} store - The redux store.
* @private
* @returns {void}
*/
function _stopNetInterception({ dispatch }) {
XHRInterceptor.disableInterception();
dispatch({
type: _REMOVE_ALL_NETWORK_REQUESTS
});
}

View File

@@ -1,60 +0,0 @@
// @flow
import { ReducerRegistry, set } from '../../base/redux';
import {
_ADD_NETWORK_REQUEST,
_REMOVE_ALL_NETWORK_REQUESTS,
_REMOVE_NETWORK_REQUEST
} from './actionTypes';
/**
* The default/initial redux state of the feature network-activity.
*
* @type {{
* requests: Map
* }}
*/
const DEFAULT_STATE = {
/**
* The ongoing network requests i.e. the network request which have been
* added to the redux store/state and have not been removed.
*
* @type {Map}
*/
requests: new Map()
};
ReducerRegistry.register(
'features/network-activity',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case _ADD_NETWORK_REQUEST: {
const {
type, // eslint-disable-line no-unused-vars
request: key,
...value
} = action;
const requests = new Map(state.requests);
requests.set(key, value);
return set(state, 'requests', requests);
}
case _REMOVE_ALL_NETWORK_REQUESTS:
return set(state, 'requests', DEFAULT_STATE.requests);
case _REMOVE_NETWORK_REQUEST: {
const { request: key } = action;
const requests = new Map(state.requests);
requests.delete(key);
return set(state, 'requests', requests);
}
}
return state;
});

View File

@@ -18,7 +18,7 @@ export function toDisplayableList(recentList) {
date: item.date,
duration: item.duration,
time: [ item.date ],
title: decodeURIComponent(parseURIString(item.conference).room),
title: parseURIString(item.conference).room,
url: item.conference
};
}));

View File

@@ -2,4 +2,5 @@
* The URL that is the main landing page for YouTube live streaming and should
* have a user's live stream key.
*/
export const YOUTUBE_LIVE_DASHBOARD_URL = 'https://www.youtube.com/live_dashboard';
export const YOUTUBE_LIVE_DASHBOARD_URL
= 'https://www.youtube.com/live_dashboard';

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