mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-09-23 11:46:59 +00:00
Compare commits
5 Commits
3391
...
base-sessi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d60a4e3a6 | ||
|
|
160380dac0 | ||
|
|
9dbba7b119 | ||
|
|
7101f90b6e | ||
|
|
621ee7b447 |
@@ -2,6 +2,5 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:navigationBarColor">#1081B2</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -74,7 +74,7 @@ if [[ $MVN_HTTP == 0 ]]; then
|
||||
popd
|
||||
|
||||
# Tag the release
|
||||
git tag android-sdk-${SDK_VERSION}
|
||||
git tag -a android-sdk-${SDK_VERSION}
|
||||
fi
|
||||
|
||||
# Done!
|
||||
|
||||
@@ -62,8 +62,6 @@ dependencies {
|
||||
implementation project(':react-native-sound')
|
||||
implementation project(':react-native-vector-icons')
|
||||
implementation project(':react-native-webrtc')
|
||||
implementation project(':react-native-webview')
|
||||
implementation project(':@react-native-community_async-storage')
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
@@ -207,7 +205,7 @@ publishing {
|
||||
def groupId = it.moduleGroup
|
||||
def artifactId = it.moduleName
|
||||
|
||||
if ((artifactId.startsWith('react-native-') || artifactId.startsWith('@react-native-community'))
|
||||
if (artifactId.startsWith('react-native-')
|
||||
&& groupId.equals('jitsi-meet')) {
|
||||
groupId = rootProject.ext.moduleGroupId
|
||||
}
|
||||
|
||||
@@ -40,15 +40,6 @@ public class JitsiMeetActivityDelegate {
|
||||
private static PermissionListener permissionListener;
|
||||
private static Callback permissionsCallback;
|
||||
|
||||
/**
|
||||
* Tells whether or not the permissions request is currently in progress.
|
||||
*
|
||||
* @return {@code true} if the permssions are being requested or {@code false} otherwise.
|
||||
*/
|
||||
static boolean arePermissionsBeingRequested() {
|
||||
return permissionListener != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Activity} lifecycle method which should be called from
|
||||
* {@code Activity#onActivityResult} so we are notified about results of external intents
|
||||
|
||||
@@ -123,7 +123,6 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener> {
|
||||
PictureInPictureModule.class);
|
||||
if (pipModule != null
|
||||
&& PictureInPictureModule.isPictureInPictureSupported()
|
||||
&& !JitsiMeetActivityDelegate.arePermissionsBeingRequested()
|
||||
&& this.url != null) {
|
||||
try {
|
||||
pipModule.enterPictureInPicture();
|
||||
|
||||
@@ -147,8 +147,6 @@ class ReactInstanceManagerHolder {
|
||||
new com.oblador.vectoricons.VectorIconsPackage(),
|
||||
new com.ocetnik.timer.BackgroundTimerPackage(),
|
||||
new com.oney.WebRTCModule.WebRTCModulePackage(),
|
||||
new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
|
||||
new com.reactnativecommunity.webview.RNCWebViewPackage(),
|
||||
new com.rnimmersive.RNImmersivePackage(),
|
||||
new com.zmxv.RNSound.RNSoundPackage(),
|
||||
new ReactPackageAdapter() {
|
||||
|
||||
@@ -3,8 +3,6 @@ rootProject.name = 'jitsi-meet'
|
||||
include ':app', ':sdk'
|
||||
include ':react-native-background-timer'
|
||||
project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
|
||||
include ':react-native-calendar-events'
|
||||
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')
|
||||
include ':react-native-fast-image'
|
||||
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
|
||||
include ':react-native-google-signin'
|
||||
@@ -21,7 +19,5 @@ include ':react-native-vector-icons'
|
||||
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
||||
include ':react-native-webrtc'
|
||||
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
|
||||
include ':react-native-webview'
|
||||
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
|
||||
include ':@react-native-community_async-storage'
|
||||
project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android')
|
||||
include ':react-native-calendar-events'
|
||||
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')
|
||||
|
||||
@@ -718,21 +718,13 @@ export default {
|
||||
this.roomName = options.roomName;
|
||||
|
||||
return (
|
||||
|
||||
// Initialize the device list first. This way, when creating tracks
|
||||
// based on preferred devices, loose label matching can be done in
|
||||
// cases where the exact ID match is no longer available, such as
|
||||
// when the camera device has switched USB ports.
|
||||
this._initDeviceList()
|
||||
.catch(error => logger.warn(
|
||||
'initial device list initialization failed', error))
|
||||
.then(() => this.createInitialLocalTracksAndConnect(
|
||||
this.createInitialLocalTracksAndConnect(
|
||||
options.roomName, {
|
||||
startAudioOnly: config.startAudioOnly,
|
||||
startScreenSharing: config.startScreenSharing,
|
||||
startWithAudioMuted: config.startWithAudioMuted,
|
||||
startWithVideoMuted: config.startWithVideoMuted
|
||||
}))
|
||||
})
|
||||
.then(([ tracks, con ]) => {
|
||||
tracks.forEach(track => {
|
||||
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|
||||
@@ -777,10 +769,7 @@ export default {
|
||||
this.setVideoMuteStatus(true);
|
||||
}
|
||||
|
||||
// Initialize device list a second time to ensure device labels
|
||||
// get populated in case of an initial gUM acceptance; otherwise
|
||||
// they may remain as empty strings.
|
||||
this._initDeviceList(true);
|
||||
this._initDeviceList();
|
||||
|
||||
if (config.iAmRecorder) {
|
||||
this.recorder = new Recorder();
|
||||
@@ -2288,23 +2277,20 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the list of current devices.
|
||||
* @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
|
||||
* Inits list of current devices and event listener for device change.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_initDeviceList(setDeviceListChangeHandler = false) {
|
||||
_initDeviceList() {
|
||||
const { mediaDevices } = JitsiMeetJS;
|
||||
|
||||
if (mediaDevices.isDeviceListAvailable()
|
||||
&& mediaDevices.isDeviceChangeAvailable()) {
|
||||
if (setDeviceListChangeHandler) {
|
||||
this.deviceChangeListener = devices =>
|
||||
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
|
||||
mediaDevices.addEventListener(
|
||||
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
|
||||
this.deviceChangeListener);
|
||||
}
|
||||
this.deviceChangeListener = devices =>
|
||||
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
|
||||
mediaDevices.addEventListener(
|
||||
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
|
||||
this.deviceChangeListener);
|
||||
|
||||
const { dispatch } = APP.store;
|
||||
|
||||
|
||||
@@ -266,9 +266,6 @@ var config = {
|
||||
// Whether or not some features are checked based on token.
|
||||
// enableFeaturesBasedOnToken: false,
|
||||
|
||||
// Enable lock room for all moderators, even when userRolesBasedOnToken is enabled and participants are guests.
|
||||
// lockRoomGuestEnabled: false,
|
||||
|
||||
// Message to show the users. Example: 'The service will be down for
|
||||
// maintenance at 01:00 AM GMT,
|
||||
// noticeMessage: '',
|
||||
|
||||
@@ -5,7 +5,8 @@ import jitsiLocalStorage from './modules/util/JitsiLocalStorage';
|
||||
|
||||
import {
|
||||
connectionEstablished,
|
||||
connectionFailed
|
||||
connectionFailed,
|
||||
connectionWillConnect
|
||||
} from './react/features/base/connection';
|
||||
import {
|
||||
isFatalJitsiConnectionError,
|
||||
@@ -74,6 +75,7 @@ function checkForAttachParametersAndConnect(id, password, connection) {
|
||||
function connect(id, password, roomName) {
|
||||
const connectionConfig = Object.assign({}, config);
|
||||
const { issuer, jwt } = APP.store.getState()['features/base/jwt'];
|
||||
const { locationURL } = APP.store.getState()['features/base/connection'];
|
||||
|
||||
connectionConfig.bosh += `?room=${roomName}`;
|
||||
|
||||
@@ -83,6 +85,8 @@ function connect(id, password, roomName) {
|
||||
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
|
||||
connectionConfig);
|
||||
|
||||
APP.store.dispatch(connectionWillConnect(connection, locationURL));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.addEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
|
||||
|
||||
194
css/_chat.scss
194
css/_chat.scss
@@ -1,9 +1,6 @@
|
||||
#sideToolbarContainer {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
box-sizing: border-box;
|
||||
color: #FFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/**
|
||||
* Make the sidebar flush with the top of the toolbar. Take the size of
|
||||
* the toolbar and subtract from 100%.
|
||||
@@ -24,6 +21,20 @@
|
||||
&.slideInExt {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sideToolbarContainer__inner {
|
||||
box-sizing: border-box;
|
||||
color: #FFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: $sidebarWidth;
|
||||
}
|
||||
}
|
||||
|
||||
#chat_container * {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
#chatconversation {
|
||||
@@ -31,8 +42,9 @@
|
||||
flex: 1;
|
||||
font-size: 10pt;
|
||||
line-height: 20px;
|
||||
margin-top: $desktopAppDragBarHeight + 5px;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
padding: 5px;
|
||||
text-align: left;
|
||||
width: $sidebarWidth;
|
||||
word-wrap: break-word;
|
||||
@@ -80,41 +92,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background-color: $chatHeaderBackgroundColor;
|
||||
height: 70px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.chat-close {
|
||||
background: gray;
|
||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 100%;
|
||||
color: white;
|
||||
cursor:pointer;
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
text-align: center;
|
||||
top: $desktopAppDragBarHeight;
|
||||
width: 10px;
|
||||
z-index: 1;
|
||||
|
||||
.chat-close {
|
||||
align-items: center;
|
||||
bottom: 8px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 18px;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
line-height: 15px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
width: 40px;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#chat-input {
|
||||
border-top: 1px solid $chatInputSeparatorColor;
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
display: flex;
|
||||
|
||||
* {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.remoteuser {
|
||||
@@ -126,13 +123,16 @@
|
||||
}
|
||||
|
||||
#usermsg {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
border: 0px none;
|
||||
border-radius:0;
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-size: 10pt;
|
||||
line-height: 30px;
|
||||
padding: 5px;
|
||||
max-height:150px;
|
||||
min-height:35px;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
@@ -145,47 +145,64 @@
|
||||
}
|
||||
|
||||
#nickname {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
color: #9d9d9d;
|
||||
font-size: 18px;
|
||||
margin-top: 30px;
|
||||
top: 100px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.sideToolbarContainer {
|
||||
* {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
#chat_container .display-name {
|
||||
float: left;
|
||||
padding-left: 5px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 95%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
#chat_container .timestamp {
|
||||
float: right;
|
||||
padding-right: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.usermessage {
|
||||
padding-top: 20px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.chatArrow {
|
||||
border-color:
|
||||
transparent $newToolbarBackgroundColor transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 0 10px 10px 0;
|
||||
left: -10px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.chatmessage {
|
||||
background-color: $chatRemoteMessageBackgroundColor;
|
||||
border-radius: 0px 6px 6px 6px;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
width: 93%;
|
||||
margin-left: 9px;
|
||||
margin-right: auto;
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0px;
|
||||
margin-top: 3px;
|
||||
max-width: 100%;
|
||||
color: white;
|
||||
padding-bottom: 3px;
|
||||
position: relative;
|
||||
|
||||
&.localuser {
|
||||
background-color: $chatLocalMessageBackgroundColor;
|
||||
border-radius: 6px 0px 6px 6px;
|
||||
&.localuser .display-name {
|
||||
color: #4C9AFF
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-radius: 0px;
|
||||
|
||||
.chatArrow,
|
||||
.timestamp,
|
||||
.display-name {
|
||||
display: none;
|
||||
@@ -214,6 +231,7 @@
|
||||
}
|
||||
|
||||
#smileysarea {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
display: flex;
|
||||
max-height: 150px;
|
||||
min-height: 35px;
|
||||
@@ -228,22 +246,14 @@
|
||||
.smileys-panel {
|
||||
bottom: 100%;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
max-height: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
transition: height 0.3s;
|
||||
width: $sidebarWidth;
|
||||
|
||||
/**
|
||||
* CSS transitions do not apply for auto dimensions. So to produce the css
|
||||
* accordion effect for showing and hiding the smiley-panel, while allowing
|
||||
* for variable panel, height, use a very large max-height and animate off
|
||||
* of that.
|
||||
*/
|
||||
transition: max-height 0.3s;
|
||||
|
||||
&.show-smileys {
|
||||
max-height: 500%;
|
||||
height: 146px;
|
||||
}
|
||||
|
||||
#smileysContainer {
|
||||
@@ -277,49 +287,3 @@
|
||||
#usermsg::-webkit-scrollbar-track-piece {
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
.chat-message-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.local {
|
||||
align-items: flex-end;
|
||||
|
||||
.chatmessage {
|
||||
background-color: $chatLocalMessageBackgroundColor;
|
||||
border-radius: 6px 0px 6px 6px;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
.chatmessage {
|
||||
border-radius: 0px;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chatmessage-wrapper {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.chatmessage {
|
||||
background-color: $chatRemoteMessageBackgroundColor;
|
||||
border-radius: 0px 6px 6px 6px;
|
||||
display: inline-block;
|
||||
margin-top: 3px;
|
||||
color: white;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,19 +83,11 @@ $modalMockAKInputBorder: 1px solid #f4f5f7;
|
||||
$modalTextColor: #333;
|
||||
|
||||
/**
|
||||
* Chat
|
||||
*/
|
||||
$chatHeaderBackgroundColor: rgba(42, 58, 75, 0.9);
|
||||
$chatInputSeparatorColor: #A4B8D1;
|
||||
$chatLocalMessageBackgroundColor: rgba(26, 108, 180, 1);
|
||||
$chatRemoteMessageBackgroundColor: rgba(240, 243, 247, 0.15);
|
||||
$sidebarWidth: 375px;
|
||||
|
||||
/**
|
||||
* Misc.
|
||||
*/
|
||||
$borderRadius: 4px;
|
||||
$defaultWatermarkLink: '../images/watermark.png';
|
||||
$sidebarWidth: 220px;
|
||||
$popoverMenuPadding: 13px;
|
||||
$happySoftwareBackground: transparent;
|
||||
$desktopAppDragBarHeight: 25px;
|
||||
|
||||
@@ -162,10 +162,4 @@ body.welcome-page {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-watermark {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,6 @@
|
||||
font-size: 12px;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
padding: 15pt;
|
||||
position: absolute;
|
||||
transform: translateY(-50%);
|
||||
top: 50%;
|
||||
|
||||
@@ -5,7 +5,7 @@ signed Android build for that, that can be a debug self-signed build too, just
|
||||
retrieve the signing hash. The key hash of an already signed ap can be obtained
|
||||
as follows (on macOS): ```keytool -list -printcert -jarfile the-app.apk```
|
||||
- Place the generated ```google-services.json``` file in ```android/app```
|
||||
for Android and the ```GoogleService-Info.plist``` into ```ios/app``` for
|
||||
for Android and the ```GoogleService-Info.plist``` into ```ios/app/src``` for
|
||||
iOS (you can stop at that step, no need for the driver and the code changes they
|
||||
suggest in the wizard).
|
||||
- You may want to exclude these files in YOUR GIT config (do not exclude them in
|
||||
|
||||
@@ -167,13 +167,7 @@ var interfaceConfig = {
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
RECENT_LIST_ENABLED: true,
|
||||
|
||||
/**
|
||||
* A UX mode where the last screen share participant is automatically
|
||||
* pinned. Note: this mode is experimental and subject to breakage.
|
||||
*/
|
||||
AUTO_PIN_LATEST_SCREEN_SHARE: true
|
||||
RECENT_LIST_ENABLED: true
|
||||
|
||||
/**
|
||||
* How many columns the tile view can expand to. The respected range is
|
||||
@@ -201,6 +195,12 @@ var interfaceConfig = {
|
||||
*/
|
||||
// ANDROID_APP_PACKAGE: 'org.jitsi.meet',
|
||||
|
||||
/**
|
||||
* A UX mode where the last screen share participant is automatically
|
||||
* pinned. Note: this mode is experimental and subject to breakage.
|
||||
*/
|
||||
// AUTO_PIN_LATEST_SCREEN_SHARE: false,
|
||||
|
||||
/**
|
||||
* Override the behavior of some notifications to remain displayed until
|
||||
* explicitly dismissed through a user action. The value is how long, in
|
||||
|
||||
47
ios/Podfile
47
ios/Podfile
@@ -14,9 +14,6 @@ end
|
||||
target 'JitsiMeet' do
|
||||
project 'sdk/sdk.xcodeproj'
|
||||
|
||||
# React Native and its dependencies
|
||||
#
|
||||
|
||||
pod 'React', :path => '../node_modules/react-native', :subspecs => [
|
||||
'Core',
|
||||
'CxxBridge',
|
||||
@@ -30,31 +27,33 @@ target 'JitsiMeet' do
|
||||
'RCTWebSocket',
|
||||
]
|
||||
pod 'yoga', :path => '../node_modules/react-native/ReactCommon/yoga'
|
||||
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
|
||||
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
|
||||
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
|
||||
|
||||
# React Native plugins
|
||||
#
|
||||
|
||||
pod 'react-native-background-timer', :path => '../node_modules/react-native-background-timer'
|
||||
pod 'react-native-calendar-events', :path => '../node_modules/react-native-calendar-events'
|
||||
pod 'react-native-fast-image', :path => '../node_modules/react-native-fast-image'
|
||||
pod 'react-native-keep-awake', :path => '../node_modules/react-native-keep-awake'
|
||||
pod 'react-native-webview', :path => '../node_modules/react-native-webview'
|
||||
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
|
||||
pod 'BVLinearGradient', :path => '../node_modules/react-native-linear-gradient'
|
||||
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-community/async-storage'
|
||||
pod 'RNGoogleSignin', :path => '../node_modules/react-native-google-signin'
|
||||
pod 'RNSound', :path => '../node_modules/react-native-sound'
|
||||
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
|
||||
pod 'RNWatch', :path => '../node_modules/react-native-watch-connectivity'
|
||||
|
||||
# Native pod dependencies
|
||||
#
|
||||
pod 'DoubleConversion',
|
||||
:podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
|
||||
pod 'glog',
|
||||
:podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
|
||||
pod 'Folly',
|
||||
:podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
|
||||
|
||||
pod 'Amplitude-iOS', '~> 4.0.4'
|
||||
pod 'ObjectiveDropboxOfficial', '~> 3.9.4'
|
||||
|
||||
pod 'react-native-background-timer',
|
||||
:path => '../node_modules/react-native-background-timer'
|
||||
pod 'react-native-fast-image',
|
||||
:path => '../node_modules/react-native-fast-image'
|
||||
pod 'react-native-keep-awake',
|
||||
:path => '../node_modules/react-native-keep-awake'
|
||||
pod 'BVLinearGradient',
|
||||
:path => '../node_modules/react-native-linear-gradient'
|
||||
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
|
||||
pod 'RNGoogleSignin',
|
||||
:path => '../node_modules/react-native-google-signin'
|
||||
pod 'RNSound', :path => '../node_modules/react-native-sound'
|
||||
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
|
||||
pod 'RNWatch', :path => '../node_modules/react-native-watch-connectivity'
|
||||
pod 'react-native-calendar-events',
|
||||
:path => '../node_modules/react-native-calendar-events'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
||||
@@ -84,8 +84,8 @@ PODS:
|
||||
- nanopb/decode (0.3.901)
|
||||
- nanopb/encode (0.3.901)
|
||||
- ObjectiveDropboxOfficial (3.9.4)
|
||||
- React (0.59.8):
|
||||
- React/Core (= 0.59.8)
|
||||
- React (0.59.5):
|
||||
- React/Core (= 0.59.5)
|
||||
- react-native-background-timer (2.1.1):
|
||||
- React
|
||||
- react-native-calendar-events (1.6.4):
|
||||
@@ -99,57 +99,53 @@ PODS:
|
||||
- React
|
||||
- react-native-webrtc (1.69.1):
|
||||
- React
|
||||
- react-native-webview (5.8.1):
|
||||
- React
|
||||
- React/Core (0.59.8):
|
||||
- yoga (= 0.59.8.React)
|
||||
- React/CxxBridge (0.59.8):
|
||||
- React/Core (0.59.5):
|
||||
- yoga (= 0.59.5.React)
|
||||
- React/CxxBridge (0.59.5):
|
||||
- Folly (= 2018.10.22.00)
|
||||
- React/Core
|
||||
- React/cxxreact
|
||||
- React/jsiexecutor
|
||||
- React/cxxreact (0.59.8):
|
||||
- React/cxxreact (0.59.5):
|
||||
- boost-for-react-native (= 1.63.0)
|
||||
- DoubleConversion
|
||||
- Folly (= 2018.10.22.00)
|
||||
- glog
|
||||
- React/jsinspector
|
||||
- React/DevSupport (0.59.8):
|
||||
- React/DevSupport (0.59.5):
|
||||
- React/Core
|
||||
- React/RCTWebSocket
|
||||
- React/fishhook (0.59.8)
|
||||
- React/jsi (0.59.8):
|
||||
- React/fishhook (0.59.5)
|
||||
- React/jsi (0.59.5):
|
||||
- DoubleConversion
|
||||
- Folly (= 2018.10.22.00)
|
||||
- glog
|
||||
- React/jsiexecutor (0.59.8):
|
||||
- React/jsiexecutor (0.59.5):
|
||||
- DoubleConversion
|
||||
- Folly (= 2018.10.22.00)
|
||||
- glog
|
||||
- React/cxxreact
|
||||
- React/jsi
|
||||
- React/jsinspector (0.59.8)
|
||||
- React/RCTActionSheet (0.59.8):
|
||||
- React/jsinspector (0.59.5)
|
||||
- React/RCTActionSheet (0.59.5):
|
||||
- React/Core
|
||||
- React/RCTAnimation (0.59.8):
|
||||
- React/RCTAnimation (0.59.5):
|
||||
- React/Core
|
||||
- React/RCTBlob (0.59.8):
|
||||
- React/RCTBlob (0.59.5):
|
||||
- React/Core
|
||||
- React/RCTImage (0.59.8):
|
||||
- React/RCTImage (0.59.5):
|
||||
- React/Core
|
||||
- React/RCTNetwork
|
||||
- React/RCTLinkingIOS (0.59.8):
|
||||
- React/RCTLinkingIOS (0.59.5):
|
||||
- React/Core
|
||||
- React/RCTNetwork (0.59.8):
|
||||
- React/RCTNetwork (0.59.5):
|
||||
- React/Core
|
||||
- React/RCTText (0.59.8):
|
||||
- React/RCTText (0.59.5):
|
||||
- React/Core
|
||||
- React/RCTWebSocket (0.59.8):
|
||||
- React/RCTWebSocket (0.59.5):
|
||||
- React/Core
|
||||
- React/fishhook
|
||||
- React/RCTBlob
|
||||
- RNCAsyncStorage (1.3.4):
|
||||
- React
|
||||
- RNGoogleSignin (1.0.2):
|
||||
- GoogleSignIn
|
||||
- React
|
||||
@@ -166,7 +162,7 @@ PODS:
|
||||
- SDWebImage/GIF (4.4.6):
|
||||
- FLAnimatedImage (~> 1.0)
|
||||
- SDWebImage/Core
|
||||
- yoga (0.59.8.React)
|
||||
- yoga (0.59.5.React)
|
||||
|
||||
DEPENDENCIES:
|
||||
- Amplitude-iOS (~> 4.0.4)
|
||||
@@ -184,7 +180,6 @@ DEPENDENCIES:
|
||||
- react-native-fast-image (from `../node_modules/react-native-fast-image`)
|
||||
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
|
||||
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
- React/Core (from `../node_modules/react-native`)
|
||||
- React/CxxBridge (from `../node_modules/react-native`)
|
||||
- React/DevSupport (from `../node_modules/react-native`)
|
||||
@@ -195,7 +190,6 @@ DEPENDENCIES:
|
||||
- React/RCTNetwork (from `../node_modules/react-native`)
|
||||
- React/RCTText (from `../node_modules/react-native`)
|
||||
- React/RCTWebSocket (from `../node_modules/react-native`)
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)"
|
||||
- RNGoogleSignin (from `../node_modules/react-native-google-signin`)
|
||||
- RNSound (from `../node_modules/react-native-sound`)
|
||||
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||
@@ -245,10 +239,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-keep-awake"
|
||||
react-native-webrtc:
|
||||
:path: "../node_modules/react-native-webrtc"
|
||||
react-native-webview:
|
||||
:path: "../node_modules/react-native-webview"
|
||||
RNCAsyncStorage:
|
||||
:path: "../node_modules/@react-native-community/async-storage"
|
||||
RNGoogleSignin:
|
||||
:path: "../node_modules/react-native-google-signin"
|
||||
RNSound:
|
||||
@@ -283,21 +273,19 @@ SPEC CHECKSUMS:
|
||||
GTMSessionFetcher: 32aeca0aa144acea523e1c8e053089dec2cb98ca
|
||||
nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48
|
||||
ObjectiveDropboxOfficial: a5afefc83f6467c42c45f2253f583f2ad1ffc701
|
||||
React: 76e6aa2b87d05eb6cccb6926d72685c9a07df152
|
||||
React: 90adac468c7b72bf1fa6c64bf230650f851a8388
|
||||
react-native-background-timer: 0d34748e53a972507c66963490c775321a88f6f2
|
||||
react-native-calendar-events: ee9573e355711ac679e071be70789542431f4ce3
|
||||
react-native-fast-image: 47487b71169aea34868e7b38bf870b6b3f2157c5
|
||||
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
|
||||
react-native-webrtc: 90a847d19deb2d7323fef8cc89ca12b8995fbc90
|
||||
react-native-webview: a95842e3f351a6d2c8bc8bcc9eab689c7e7e5ad4
|
||||
RNCAsyncStorage: 8e31405a9f12fbf42c2bb330e4560bfd79c18323
|
||||
RNGoogleSignin: 361174d9a3090d295b06257162b560d8efc8a6ed
|
||||
RNSound: e157320f503bdd4f4ee6d8542e948d54f90c3c3a
|
||||
RNVectorIcons: d819334932bcda3332deb3d2c8ea4d069e0b98f9
|
||||
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
|
||||
SDWebImage: 3f3f0c02f09798048c47a5ed0a13f17b063572d8
|
||||
yoga: 92b2102c3d373d1a790db4ab761d2b0ffc634f64
|
||||
yoga: 2e571f113e8cbeb0eb752aeebc86c1bfe7a8200c
|
||||
|
||||
PODFILE CHECKSUM: b55338cc43312051ed83f8d9c6aadbd8c9402e6a
|
||||
PODFILE CHECKSUM: 9e6bc935ea7d2974604572cc68938281a88cf35c
|
||||
|
||||
COCOAPODS: 1.6.1
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script will download a bitcode build of the WebRTC framework, if needed.
|
||||
|
||||
if [[ ! "$CONFIGURATION" = "Debug" ]]; then
|
||||
RN_WEBRTC="$SRCROOT/../../node_modules/react-native-webrtc"
|
||||
|
||||
if otool -arch arm64 -l $RN_WEBRTC/ios/WebRTC.framework/WebRTC | grep -q LLVM; then
|
||||
echo "WebRTC framework has bitcode"
|
||||
else
|
||||
echo "WebRTC framework has NO bitcode"
|
||||
$RN_WEBRTC/tools/downloadBitcode.sh
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -5,8 +5,7 @@ set -e -u
|
||||
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
|
||||
PROJECT_REPO=$(realpath ${THIS_DIR}/../..)
|
||||
RELEASE_REPO=$(realpath ${THIS_DIR}/../../../jitsi-meet-ios-sdk-releases)
|
||||
DEFAULT_SDK_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${THIS_DIR}/../sdk/src/Info.plist)
|
||||
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
|
||||
SDK_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${THIS_DIR}/../sdk/src/Info.plist)
|
||||
|
||||
|
||||
echo "Releasing Jitsi Meet SDK ${SDK_VERSION}"
|
||||
@@ -25,7 +24,7 @@ popd
|
||||
pushd ${PROJECT_REPO}
|
||||
rm -rf ios/sdk/JitsiMeet.framework
|
||||
xcodebuild -workspace ios/jitsi-meet.xcworkspace -scheme JitsiMeet -destination='generic/platform=iOS' -configuration Release archive
|
||||
git tag ios-sdk-${SDK_VERSION}
|
||||
git tag -a ios-sdk-${SDK_VERSION}
|
||||
popd
|
||||
|
||||
pushd ${RELEASE_REPO}
|
||||
@@ -34,10 +33,6 @@ pushd ${RELEASE_REPO}
|
||||
cp -r ${PROJECT_REPO}/ios/sdk/JitsiMeet.framework Frameworks/
|
||||
cp -r ${PROJECT_REPO}/node_modules/react-native-webrtc/ios/WebRTC.framework Frameworks/
|
||||
|
||||
# Strip bitcode
|
||||
xcrun bitcode_strip -r Frameworks/JitsiMeet.framework/JitsiMeet -o Frameworks/JitsiMeet.framework/JitsiMeet
|
||||
xcrun bitcode_strip -r Frameworks/WebRTC.framework/WebRTC -o Frameworks/WebRTC.framework/WebRTC
|
||||
|
||||
# Add all files to git
|
||||
git add -A .
|
||||
git commit -m "${SDK_VERSION}"
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
buildConfigurationList = 0BD906ED1EC0C00300C8C18E /* Build configuration list for PBXNativeTarget "JitsiMeet" */;
|
||||
buildPhases = (
|
||||
26796D8589142D80C8AFDA51 /* [CP] Check Pods Manifest.lock */,
|
||||
DE3D81D6228B50FB00A6C149 /* Bitcode */,
|
||||
0BD906E01EC0C00300C8C18E /* Sources */,
|
||||
0BD906E11EC0C00300C8C18E /* Frameworks */,
|
||||
0BD906E21EC0C00300C8C18E /* Headers */,
|
||||
@@ -451,24 +450,6 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
DE3D81D6228B50FB00A6C149 /* Bitcode */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = Bitcode;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "../scripts/bitcode.sh\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
#import <React/RCTUtils.h>
|
||||
#import <WebRTC/WebRTC.h>
|
||||
|
||||
#import <JitsiMeet/JitsiMeet-Swift.h>
|
||||
|
||||
@@ -308,35 +307,21 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
|
||||
startedConnectingAt:nil];
|
||||
}
|
||||
|
||||
- (void) providerDidActivateAudioSessionWithSession:(AVAudioSession *)session {
|
||||
// The following just help with debugging:
|
||||
#ifdef DEBUG
|
||||
|
||||
- (void) providerDidActivateAudioSessionWithSession:(AVAudioSession *)session {
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession:]");
|
||||
#endif
|
||||
[[RTCAudioSession sharedInstance] audioSessionDidActivate:session];
|
||||
}
|
||||
|
||||
- (void) providerDidDeactivateAudioSessionWithSession:(AVAudioSession *)session {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]");
|
||||
#endif
|
||||
[[RTCAudioSession sharedInstance] audioSessionDidDeactivate:session];
|
||||
}
|
||||
|
||||
- (void) providerTimedOutPerformingActionWithAction:(CXAction *)action {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]");
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
// The bridge might already be invalidated by the time a CallKit event is processed,
|
||||
// just ignore it and don't emit it.
|
||||
- (void)sendEventWithName:(NSString *)name body:(id)body {
|
||||
if (!self.bridge) {
|
||||
return;
|
||||
}
|
||||
|
||||
[super sendEventWithName:name body:body];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -98,13 +98,11 @@ curl -L -o ${CERT_DIR}/AppleWWDRCA.cer 'http://developer.apple.com/certification
|
||||
curl -L -o ${CERT_DIR}/dev-cert.cer.enc ${IOS_DEV_CERT_URL}
|
||||
curl -L -o ${CERT_DIR}/dev-key.p12.enc ${IOS_DEV_CERT_KEY_URL}
|
||||
curl -L -o ${CERT_DIR}/dev-profile.mobileprovision.enc ${IOS_DEV_PROV_PROFILE_URL}
|
||||
curl -L -o ${CERT_DIR}/dev-watch-profile.mobileprovision.enc ${IOS_DEV_WATCH_PROV_PROFILE_URL}
|
||||
curl -L -o ${CERT_DIR}/id_rsa.enc ${DEPLOY_SSH_CERT_URL}
|
||||
|
||||
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-cert.cer.enc -d -a -out ${CERT_DIR}/dev-cert.cer
|
||||
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-key.p12.enc -d -a -out ${CERT_DIR}/dev-key.p12
|
||||
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-profile.mobileprovision.enc -d -a -out ${CERT_DIR}/dev-profile.mobileprovision
|
||||
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-watch-profile.mobileprovision.enc -d -a -out ${CERT_DIR}/dev-watch-profile.mobileprovision
|
||||
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/id_rsa.enc -d -a -out ${CERT_DIR}/id_rsa
|
||||
chmod 0600 ${CERT_DIR}/id_rsa
|
||||
|
||||
@@ -128,13 +126,9 @@ echo "done set-key-partition-list"
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
||||
cp "${CERT_DIR}/dev-profile.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/
|
||||
cp "${CERT_DIR}/dev-watch-profile.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/
|
||||
|
||||
npm install
|
||||
|
||||
# Ever since the Apple Watch app has been added the bitcode for WebRTC needs to be downloaded in order to build successfully
|
||||
./node_modules/react-native-webrtc/tools/downloadBitcode.sh
|
||||
|
||||
cd ios
|
||||
pod update
|
||||
pod install
|
||||
@@ -152,12 +146,4 @@ xcodebuild -quiet -exportArchive -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchi
|
||||
|
||||
echo "Will try deploy the .ipa to: ${IPA_DEPLOY_LOCATION}"
|
||||
|
||||
ssh-add ${CERT_DIR}/id_rsa
|
||||
|
||||
if [ ! -z ${SCP_PROXY_HOST} ];
|
||||
then
|
||||
scp -o ProxyCommand="ssh -t -A -l %r ${SCP_PROXY_HOST} -o \"StrictHostKeyChecking no\" -o \"BatchMode yes\" -W %h:%p" -o StrictHostKeyChecking=no -o LogLevel=DEBUG "${IPA_EXPORT_DIR}/jitsi-meet.ipa" "${IPA_DEPLOY_LOCATION}"
|
||||
else
|
||||
scp -o StrictHostKeyChecking=no -o LogLevel=DEBUG "${IPA_EXPORT_DIR}/jitsi-meet.ipa" "${IPA_DEPLOY_LOCATION}"
|
||||
fi
|
||||
|
||||
scp -i ${CERT_DIR}/id_rsa -o StrictHostKeyChecking=no -o LogLevel=DEBUG "${IPA_EXPORT_DIR}/jitsi-meet.ipa" "${IPA_DEPLOY_LOCATION}"
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
},
|
||||
"chat": {
|
||||
"error": "Error: your message \"__originalText__\" was not sent. Reason: __error__",
|
||||
"messagebox": "Type a message",
|
||||
"messagebox": "Enter text...",
|
||||
"nickname": {
|
||||
"popover": "Choose a nickname",
|
||||
"title": "Enter a nickname to use chat"
|
||||
@@ -351,15 +351,12 @@
|
||||
"dialInConferenceID": "PIN:",
|
||||
"dialInNotSupported": "Sorry, dialing in is currently not supported.",
|
||||
"dialInNumber": "Dial-in:",
|
||||
"dialInSummaryError": "Error fetching dial-in info now. Please try again later.",
|
||||
"dialInTollFree": "Toll Free",
|
||||
"genericError": "Whoops, something went wrong.",
|
||||
"inviteLiveStream": "To view the live stream of this meeting, click this link: __url__",
|
||||
"invitePhone": "One tap audio Dial In: __number__,,__conferenceID__#",
|
||||
"invitePhoneAlternatives": "Looking for a different dial in number? Please see: __url__",
|
||||
"inviteURLFirstPartGeneral": "You are invited to join a meeting.",
|
||||
"inviteURLFirstPartPersonal": "__name__ is inviting you to a meeting.",
|
||||
"inviteURLSecondPart": "\n__moreInfo__\nJoin meeting: __url__\n",
|
||||
"inviteURL": "You are invited to join a meeting.\n__moreInfo__\nJoin meeting: __url__\n",
|
||||
"inviteURLMoreInfo": "Meeting ID: __conferenceID__#\n",
|
||||
"liveStreamURL": "Live stream:",
|
||||
"moreNumbers": "More numbers",
|
||||
@@ -776,7 +773,6 @@
|
||||
"enterRoomTitle": "Start a new meeting",
|
||||
"go": "GO",
|
||||
"join": "JOIN",
|
||||
"info": "Info",
|
||||
"privacy": "Privacy",
|
||||
"recentList": "Recent",
|
||||
"recentListDelete": "Delete",
|
||||
|
||||
@@ -166,6 +166,22 @@ function getCameraVideoPosition( // eslint-disable-line max-params
|
||||
verticalIndent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of the video horizontal and vertical indents.
|
||||
* Centers horizontally and top aligns vertically.
|
||||
*
|
||||
* @return an array with 2 elements, the horizontal indent and the vertical
|
||||
* indent
|
||||
*/
|
||||
function getDesktopVideoPosition(videoWidth, videoHeight, videoSpaceWidth) {
|
||||
const horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
|
||||
|
||||
const verticalIndent = 0;// Top aligned
|
||||
|
||||
return { horizontalIndent,
|
||||
verticalIndent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for user video.
|
||||
*/
|
||||
@@ -350,23 +366,23 @@ export class VideoContainer extends LargeContainer {
|
||||
* @returns {{horizontalIndent, verticalIndent}}
|
||||
*/
|
||||
getVideoPosition(width, height, containerWidth, containerHeight) {
|
||||
let containerWidthToUse = containerWidth;
|
||||
|
||||
/* eslint-enable max-params */
|
||||
if (this.stream && this.isScreenSharing()) {
|
||||
let availableContainerWidth = containerWidth;
|
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
containerWidthToUse -= Filmstrip.getFilmstripWidth();
|
||||
availableContainerWidth -= Filmstrip.getFilmstripWidth();
|
||||
}
|
||||
|
||||
return getCameraVideoPosition(width,
|
||||
return getDesktopVideoPosition(width,
|
||||
height,
|
||||
containerWidthToUse,
|
||||
availableContainerWidth,
|
||||
containerHeight);
|
||||
}
|
||||
|
||||
return getCameraVideoPosition(width,
|
||||
height,
|
||||
containerWidthToUse,
|
||||
containerWidth,
|
||||
containerHeight);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
/* global APP, JitsiMeetJS */
|
||||
|
||||
import { getAudioOutputDeviceId } from '../../react/features/base/devices';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
getUserSelectedMicDeviceId,
|
||||
getUserSelectedOutputDeviceId
|
||||
} from '../../react/features/base/settings';
|
||||
|
||||
/**
|
||||
* Determines if currently selected audio output device should be changed after
|
||||
@@ -31,7 +26,8 @@ function getNewAudioOutputDevice(newDevices) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
const preferredAudioOutputDeviceId = getUserSelectedOutputDeviceId(APP.store.getState());
|
||||
const settings = APP.store.getState()['features/base/settings'];
|
||||
const preferredAudioOutputDeviceId = settings.userSelectedAudioOutputDeviceId;
|
||||
|
||||
// if the preferred one is not the selected and is available in the new devices
|
||||
// we want to use it as it was just added
|
||||
@@ -53,7 +49,8 @@ function getNewAudioOutputDevice(newDevices) {
|
||||
function getNewAudioInputDevice(newDevices, localAudio) {
|
||||
const availableAudioInputDevices = newDevices.filter(
|
||||
d => d.kind === 'audioinput');
|
||||
const selectedAudioInputDeviceId = getUserSelectedMicDeviceId(APP.store.getState());
|
||||
const settings = APP.store.getState()['features/base/settings'];
|
||||
const selectedAudioInputDeviceId = settings.userSelectedMicDeviceId;
|
||||
const selectedAudioInputDevice = availableAudioInputDevices.find(
|
||||
d => d.deviceId === selectedAudioInputDeviceId);
|
||||
|
||||
@@ -91,7 +88,8 @@ function getNewAudioInputDevice(newDevices, localAudio) {
|
||||
function getNewVideoInputDevice(newDevices, localVideo) {
|
||||
const availableVideoInputDevices = newDevices.filter(
|
||||
d => d.kind === 'videoinput');
|
||||
const selectedVideoInputDeviceId = getUserSelectedCameraDeviceId(APP.store.getState());
|
||||
const settings = APP.store.getState()['features/base/settings'];
|
||||
const selectedVideoInputDeviceId = settings.userSelectedCameraDeviceId;
|
||||
const selectedVideoInputDevice = availableVideoInputDevices.find(
|
||||
d => d.deviceId === selectedVideoInputDeviceId);
|
||||
|
||||
|
||||
125
package-lock.json
generated
125
package-lock.json
generated
@@ -2448,15 +2448,10 @@
|
||||
"isomorphic-fetch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"@react-native-community/async-storage": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/async-storage/-/async-storage-1.3.4.tgz",
|
||||
"integrity": "sha512-fJmzL27x0BEjhmMXPnDPnUNCZK7bph+NBVCfAz9fzHzAamaiOkdUwuL3PvE4Oj4Kw4knP8ocw5VRDGorAidZ2g=="
|
||||
},
|
||||
"@react-native-community/cli": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-1.9.4.tgz",
|
||||
"integrity": "sha512-7XjgqCdi23g6V7RV4tsYvqVqOBtNjAsWe5Oj2dR5KxDi3YqUyIyPjDWzyFkIxiO9XTGp9Al4QSmRwtOERvHO8A==",
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-1.9.2.tgz",
|
||||
"integrity": "sha512-wSw3g6HrSUvLZiHiWRcO++JrKdbYNRWycGbGHVCnRLsdDRsj/y152xPlvBa29C8w+1SwiiN8aGsBOO0x9hkrCg==",
|
||||
"requires": {
|
||||
"chalk": "^1.1.1",
|
||||
"commander": "^2.19.0",
|
||||
@@ -2840,7 +2835,7 @@
|
||||
"blueimp-md5": "^2.10.0",
|
||||
"json3": "^3.3.2",
|
||||
"lodash": "^4.17.4",
|
||||
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d"
|
||||
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ua-parser-js": {
|
||||
@@ -5485,41 +5480,12 @@
|
||||
}
|
||||
},
|
||||
"errorhandler": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz",
|
||||
"integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.0.tgz",
|
||||
"integrity": "sha1-6rpkyl1UKjEayUX1gt78M2Fl2fQ=",
|
||||
"requires": {
|
||||
"accepts": "~1.3.7",
|
||||
"accepts": "~1.3.3",
|
||||
"escape-html": "~1.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
|
||||
"requires": {
|
||||
"mime-types": "~2.1.24",
|
||||
"negotiator": "0.6.2"
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.24",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
||||
"requires": {
|
||||
"mime-db": "1.40.0"
|
||||
}
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"es-abstract": {
|
||||
@@ -6750,7 +6716,8 @@
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -6768,11 +6735,13 @@
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -6785,15 +6754,18 @@
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -6896,7 +6868,8 @@
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -6906,6 +6879,7 @@
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -6918,17 +6892,20 @@
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -6945,6 +6922,7 @@
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -7017,7 +6995,8 @@
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -7027,6 +7006,7 @@
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -7102,7 +7082,8 @@
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -7132,6 +7113,7 @@
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -7149,6 +7131,7 @@
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -7187,11 +7170,13 @@
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.2",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12053,9 +12038,9 @@
|
||||
}
|
||||
},
|
||||
"react-native": {
|
||||
"version": "0.59.8",
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.59.8.tgz",
|
||||
"integrity": "sha512-x1T+/pEXrjgdH9uDzd5doJy5aFlBqW04j7ljDKIGALchhnvdFbtXXrUZ/1PfWHMrIdZxtaDt4tkSttp662GSQA==",
|
||||
"version": "0.59.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.59.5.tgz",
|
||||
"integrity": "sha512-8Q/9cS6IMsGNiFhJgzmncbUeuacXQMe5EJl0c63fW30DvjEjeTVCvhM08eGzSpsNlOvL2XDRa4YOiCrwI7S1TA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@react-native-community/cli": "^1.2.1",
|
||||
@@ -12289,25 +12274,6 @@
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"react-native-webview": {
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-5.8.1.tgz",
|
||||
"integrity": "sha512-b6pSvmjoiWtcz6YspggW02X+BRXJWuquHwkh37BRx1NMW1iwMZA31SnFQvTpPzWYYIb9WF/mRsy2nGtt9C6NIg==",
|
||||
"requires": {
|
||||
"escape-string-regexp": "1.0.5",
|
||||
"invariant": "2.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-node-resolver": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-node-resolver/-/react-node-resolver-1.0.1.tgz",
|
||||
@@ -12379,15 +12345,6 @@
|
||||
"exenv": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"react-textarea-autosize": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.0.tgz",
|
||||
"integrity": "sha512-c2FlR/fP0qbxmlrW96SdrbgP/v0XZMTupqB90zybvmDVDutytUgPl7beU35klwcTeMepUIQEpQUn3P3bdshGPg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"prop-types": "^15.6.0"
|
||||
}
|
||||
},
|
||||
"react-transform-hmr": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-transform-hmr/-/react-transform-hmr-1.0.4.tgz",
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"@atlaskit/toggle": "5.0.14",
|
||||
"@atlaskit/tooltip": "12.1.13",
|
||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||
"@react-native-community/async-storage": "1.3.4",
|
||||
"@webcomponents/url": "0.7.1",
|
||||
"amplitude-js": "4.5.2",
|
||||
"bc-css-flags": "3.0.0",
|
||||
@@ -63,7 +62,7 @@
|
||||
"react-emoji-render": "0.4.6",
|
||||
"react-i18next": "7.13.0",
|
||||
"react-linkify": "0.2.2",
|
||||
"react-native": "0.59.8",
|
||||
"react-native": "0.59.5",
|
||||
"react-native-background-timer": "2.1.1",
|
||||
"react-native-calendar-events": "1.6.4",
|
||||
"react-native-callstats": "3.58.2",
|
||||
@@ -77,9 +76,7 @@
|
||||
"react-native-vector-icons": "6.0.2",
|
||||
"react-native-watch-connectivity": "0.2.0",
|
||||
"react-native-webrtc": "github:jitsi/react-native-webrtc#4064c6f2db4f8b961daaaa8dafc6a896d7cfbc43",
|
||||
"react-native-webview": "5.8.1",
|
||||
"react-redux": "5.0.7",
|
||||
"react-textarea-autosize": "7.1.0",
|
||||
"react-transition-group": "2.4.0",
|
||||
"redux": "4.0.0",
|
||||
"redux-thunk": "2.2.0",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { appWillNavigate } from '../base/app';
|
||||
import { setRoom } from '../base/conference';
|
||||
import {
|
||||
configWillLoad,
|
||||
@@ -58,6 +59,9 @@ export function appNavigate(uri: ?string) {
|
||||
const { contextRoot, host, room } = location;
|
||||
const locationURL = new URL(location.toString());
|
||||
|
||||
// XXX this looks like CONFIG_WILL_LOAD ?
|
||||
dispatch(appWillNavigate(locationURL, room));
|
||||
|
||||
dispatch(configWillLoad(locationURL, room));
|
||||
|
||||
let protocol = location.protocol.toLowerCase();
|
||||
@@ -81,7 +85,7 @@ export function appNavigate(uri: ?string) {
|
||||
config = restoreConfig(baseURL);
|
||||
|
||||
if (!config) {
|
||||
dispatch(loadConfigError(error, locationURL));
|
||||
dispatch(loadConfigError(error, locationURL, room));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -92,7 +96,7 @@ export function appNavigate(uri: ?string) {
|
||||
dispatch(setConfig(config));
|
||||
dispatch(setRoom(room));
|
||||
} else {
|
||||
dispatch(loadConfigError(new Error('Config no longer needed!'), locationURL));
|
||||
dispatch(loadConfigError(new Error('Config no longer needed!'), locationURL, room));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ export function cancelWaitForOwner() {
|
||||
// clients/consumers need an event.
|
||||
const { authRequired } = getState()['features/base/conference'];
|
||||
|
||||
// FIXME remove when external-api ported to base/session
|
||||
authRequired && dispatch(conferenceLeft(authRequired));
|
||||
|
||||
dispatch(appNavigate(undefined));
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
*/
|
||||
export const APP_WILL_MOUNT = 'APP_WILL_MOUNT';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const APP_WILL_NAVIGATE = 'APP_WILL_NAVIGATE';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific App will unmount (in
|
||||
* React terms).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
|
||||
import { APP_WILL_MOUNT, APP_WILL_NAVIGATE, APP_WILL_UNMOUNT } from './actionTypes';
|
||||
|
||||
declare var APP;
|
||||
|
||||
@@ -33,6 +33,25 @@ export function appWillMount(app: Object) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {URL} locationURL - FIXME.
|
||||
* @param {string} room - FIXME.
|
||||
* @returns {{
|
||||
* type: APP_WILL_NAVIGATE,
|
||||
* locationURL: URL,
|
||||
* room: ?string
|
||||
* }}
|
||||
*/
|
||||
export function appWillNavigate(locationURL: URL, room: ?string) {
|
||||
return {
|
||||
type: APP_WILL_NAVIGATE,
|
||||
locationURL,
|
||||
room
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a specific App will unmount (in the terms of React).
|
||||
*
|
||||
|
||||
@@ -38,17 +38,20 @@ export function configWillLoad(locationURL: URL, room: string) {
|
||||
* loading of a configuration.
|
||||
* @param {URL} locationURL - The URL of the location which necessitated the
|
||||
* loading of a configuration.
|
||||
* @param {string} room - The name of the conference room.
|
||||
* @returns {{
|
||||
* type: LOAD_CONFIG_ERROR,
|
||||
* error: Error,
|
||||
* locationURL: URL
|
||||
* locationURL: URL,
|
||||
* room: string
|
||||
* }}
|
||||
*/
|
||||
export function loadConfigError(error: Error, locationURL: URL) {
|
||||
export function loadConfigError(error: Error, locationURL: URL, room: ?string) {
|
||||
return {
|
||||
type: LOAD_CONFIG_ERROR,
|
||||
error,
|
||||
locationURL
|
||||
locationURL,
|
||||
room
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
import _ from 'lodash';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
conferenceLeft,
|
||||
conferenceWillLeave,
|
||||
getCurrentConference
|
||||
} from '../conference';
|
||||
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
|
||||
import { parseURIString } from '../util';
|
||||
|
||||
@@ -20,7 +15,7 @@ import {
|
||||
} from './actionTypes';
|
||||
import { JITSI_CONNECTION_URL_KEY } from './constants';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
import type JitsiConnection from 'lib-jitsi-meet/JitsiConnection';
|
||||
|
||||
/**
|
||||
* The error structure passed to the {@link connectionFailed} action.
|
||||
@@ -90,7 +85,7 @@ export function connect(id: ?string, password: ?string) {
|
||||
|
||||
connection[JITSI_CONNECTION_URL_KEY] = locationURL;
|
||||
|
||||
dispatch(_connectionWillConnect(connection));
|
||||
dispatch(connectionWillConnect(connection, locationURL));
|
||||
|
||||
connection.addEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_DISCONNECTED,
|
||||
@@ -102,7 +97,7 @@ export function connect(id: ?string, password: ?string) {
|
||||
JitsiConnectionEvents.CONNECTION_FAILED,
|
||||
_onConnectionFailed);
|
||||
|
||||
return connection.connect({
|
||||
connection.connect({
|
||||
id,
|
||||
password
|
||||
});
|
||||
@@ -258,16 +253,18 @@ export function connectionFailed(
|
||||
*
|
||||
* @param {JitsiConnection} connection - The {@code JitsiConnection} which will
|
||||
* connect.
|
||||
* @param {URL} locationURL - FIXME.
|
||||
* @private
|
||||
* @returns {{
|
||||
* type: CONNECTION_WILL_CONNECT,
|
||||
* connection: JitsiConnection
|
||||
* }}
|
||||
*/
|
||||
function _connectionWillConnect(connection) {
|
||||
export function connectionWillConnect(connection: JitsiConnection, locationURL: URL) {
|
||||
return {
|
||||
type: CONNECTION_WILL_CONNECT,
|
||||
connection
|
||||
connection,
|
||||
locationURL
|
||||
};
|
||||
}
|
||||
|
||||
@@ -319,67 +316,6 @@ function _constructOptions(state) {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes connection.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function disconnect() {
|
||||
return (dispatch: Dispatch<any>, getState: Function): Promise<void> => {
|
||||
const state = getState();
|
||||
|
||||
// The conference we have already joined or are joining.
|
||||
const conference_ = getCurrentConference(state);
|
||||
|
||||
// Promise which completes when the conference has been left and the
|
||||
// connection has been disconnected.
|
||||
let promise;
|
||||
|
||||
// Leave the conference.
|
||||
if (conference_) {
|
||||
// In a fashion similar to JitsiConference's CONFERENCE_LEFT event
|
||||
// (and the respective Redux action) which is fired after the
|
||||
// conference has been left, notify the application about the
|
||||
// intention to leave the conference.
|
||||
dispatch(conferenceWillLeave(conference_));
|
||||
|
||||
promise
|
||||
= conference_.leave()
|
||||
.catch(error => {
|
||||
logger.warn(
|
||||
'JitsiConference.leave() rejected with:',
|
||||
error);
|
||||
|
||||
// The library lib-jitsi-meet failed to make the
|
||||
// JitsiConference leave. Which may be because
|
||||
// JitsiConference thinks it has already left.
|
||||
// Regardless of the failure reason, continue in
|
||||
// jitsi-meet as if the leave has succeeded.
|
||||
dispatch(conferenceLeft(conference_));
|
||||
});
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
// Disconnect the connection.
|
||||
const { connecting, connection } = state['features/base/connection'];
|
||||
|
||||
// The connection we have already connected or are connecting.
|
||||
const connection_ = connection || connecting;
|
||||
|
||||
if (connection_) {
|
||||
promise = promise.then(() => connection_.disconnect());
|
||||
} else {
|
||||
// FIXME: We have no connection! Fake a disconnect. Because of how the current disconnec is implemented
|
||||
// (by doing the diconnect() in the Conference component unmount) we have lost the location URL already.
|
||||
// Oh well, at least send the event.
|
||||
promise.then(() => dispatch(_connectionDisconnected({}, '')));
|
||||
}
|
||||
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the location URL of the application, connecton, conference, etc.
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@ import { configureInitialDevices } from '../devices';
|
||||
export {
|
||||
connectionEstablished,
|
||||
connectionFailed,
|
||||
connectionWillConnect,
|
||||
setLocationURL
|
||||
} from './actions.native';
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import {
|
||||
getUserSelectedOutputDeviceId,
|
||||
updateSettings
|
||||
} from '../settings';
|
||||
import { updateSettings } from '../settings';
|
||||
|
||||
import {
|
||||
ADD_PENDING_DEVICE_REQUEST,
|
||||
@@ -94,7 +91,8 @@ export function configureInitialDevices() {
|
||||
|
||||
return updateSettingsPromise
|
||||
.then(() => {
|
||||
const userSelectedAudioOutputDeviceId = getUserSelectedOutputDeviceId(getState());
|
||||
const { userSelectedAudioOutputDeviceId }
|
||||
= getState()['features/base/settings'];
|
||||
|
||||
return setAudioOutputDeviceId(userSelectedAudioOutputDeviceId, dispatch)
|
||||
.catch(ex => logger.warn(`Failed to set audio output device.
|
||||
|
||||
@@ -67,34 +67,6 @@ export function getDeviceIdByLabel(state: Object, label: string, kind: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a device with a label that matches the passed id and returns its label.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {string} id - The device id.
|
||||
* @param {string} kind - The type of the device. One of "audioInput",
|
||||
* "audioOutput", and "videoInput". Also supported is all lowercase versions
|
||||
* of the preceding types.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function getDeviceLabelById(state: Object, id: string, kind: string) {
|
||||
const webrtcKindToJitsiKindTranslator = {
|
||||
audioinput: 'audioInput',
|
||||
audiooutput: 'audioOutput',
|
||||
videoinput: 'videoInput'
|
||||
};
|
||||
|
||||
const kindToSearch = webrtcKindToJitsiKindTranslator[kind] || kind;
|
||||
|
||||
const device
|
||||
= (state['features/base/devices'].availableDevices[kindToSearch] || [])
|
||||
.find(d => d.deviceId === id);
|
||||
|
||||
if (device) {
|
||||
return device.label;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the devices set in the URL.
|
||||
*
|
||||
@@ -146,29 +118,24 @@ export function groupDevicesByKind(devices: Object[]): Object {
|
||||
* @param {string} newId - New audio output device id.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @param {boolean} userSelection - Whether this is a user selection update.
|
||||
* @param {?string} newLabel - New audio output device label to store.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function setAudioOutputDeviceId(
|
||||
newId: string = 'default',
|
||||
dispatch: Function,
|
||||
userSelection: boolean = false,
|
||||
newLabel: ?string): Promise<*> {
|
||||
userSelection: boolean = false): Promise<*> {
|
||||
return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
|
||||
.then(() => {
|
||||
const newSettings = {
|
||||
audioOutputDeviceId: newId,
|
||||
userSelectedAudioOutputDeviceId: undefined,
|
||||
userSelectedAudioOutputDeviceLabel: undefined
|
||||
userSelectedAudioOutputDeviceId: undefined
|
||||
};
|
||||
|
||||
if (userSelection) {
|
||||
newSettings.userSelectedAudioOutputDeviceId = newId;
|
||||
newSettings.userSelectedAudioOutputDeviceLabel = newLabel;
|
||||
} else {
|
||||
// a flow workaround, I needed to add 'userSelectedAudioOutputDeviceId: undefined'
|
||||
delete newSettings.userSelectedAudioOutputDeviceId;
|
||||
delete newSettings.userSelectedAudioOutputDeviceLabel;
|
||||
}
|
||||
|
||||
return dispatch(updateSettings(newSettings));
|
||||
|
||||
@@ -147,8 +147,7 @@ function _useDevice({ dispatch }, device) {
|
||||
switch (device.kind) {
|
||||
case 'videoinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: device.deviceId,
|
||||
userSelectedCameraDeviceLabel: device.label
|
||||
userSelectedCameraDeviceId: device.deviceId
|
||||
}));
|
||||
|
||||
dispatch(setVideoInputDevice(device.deviceId));
|
||||
@@ -156,8 +155,7 @@ function _useDevice({ dispatch }, device) {
|
||||
}
|
||||
case 'audioinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedMicDeviceId: device.deviceId,
|
||||
userSelectedMicDeviceLabel: device.label
|
||||
userSelectedMicDeviceId: device.deviceId
|
||||
}));
|
||||
|
||||
dispatch(setAudioInputDevice(device.deviceId));
|
||||
@@ -167,8 +165,7 @@ function _useDevice({ dispatch }, device) {
|
||||
setAudioOutputDeviceId(
|
||||
device.deviceId,
|
||||
dispatch,
|
||||
true,
|
||||
device.label)
|
||||
true)
|
||||
.then(() => logger.log('changed audio output device'))
|
||||
.catch(err => {
|
||||
logger.warn(
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { translate } from '../../../i18n';
|
||||
import { connect } from '../../../redux';
|
||||
|
||||
import { _abstractMapStateToProps } from '../../functions';
|
||||
|
||||
import { type Props as AbstractProps } from './BaseDialog';
|
||||
import BaseSubmitDialog from './BaseSubmitDialog';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* Untranslated i18n key of the content to be displayed.
|
||||
*
|
||||
* NOTE: This dialog also adds support to Object type keys that will be
|
||||
* translated using the provided params. See i18n function
|
||||
* {@code translate(string, Object)} for more details.
|
||||
*/
|
||||
contentKey: string | { key: string, params: Object},
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an alert dialog, to simply show an error or a message, then disappear on dismiss.
|
||||
*/
|
||||
class AlertDialog extends BaseSubmitDialog<Props, *> {
|
||||
/**
|
||||
* Implements {@code BaseSubmitDialog._renderSubmittable}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderSubmittable() {
|
||||
const { _dialogStyles, contentKey, t } = this.props;
|
||||
const content
|
||||
= typeof contentKey === 'string'
|
||||
? t(contentKey)
|
||||
: this._renderHTML(t(contentKey.key, contentKey.params));
|
||||
|
||||
return (
|
||||
<Text style = { _dialogStyles.text }>
|
||||
{ content }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
_renderHTML: string => Object | string
|
||||
}
|
||||
|
||||
export default translate(connect(_abstractMapStateToProps)(AlertDialog));
|
||||
@@ -4,7 +4,6 @@ export { default as BottomSheet } from './BottomSheet';
|
||||
export { default as ConfirmDialog } from './ConfirmDialog';
|
||||
export { default as CustomDialog } from './CustomDialog';
|
||||
export { default as DialogContainer } from './DialogContainer';
|
||||
export { default as AlertDialog } from './AlertDialog';
|
||||
export { default as InputDialog } from './InputDialog';
|
||||
export { default as CustomSubmitDialog } from './CustomSubmitDialog';
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Describes audio element interface used in the base/media feature for audio
|
||||
* playback.
|
||||
@@ -12,7 +10,7 @@ export type AudioElement = {
|
||||
currentTime: number,
|
||||
pause: () => void,
|
||||
play: () => void,
|
||||
setSinkId?: string => Function,
|
||||
setSinkId?: string => void,
|
||||
stop: () => void
|
||||
};
|
||||
|
||||
@@ -115,8 +113,7 @@ export default class AbstractAudio extends Component<Props> {
|
||||
setSinkId(sinkId: string): void {
|
||||
this._audioElementImpl
|
||||
&& typeof this._audioElementImpl.setSinkId === 'function'
|
||||
&& this._audioElementImpl.setSinkId(sinkId)
|
||||
.catch(error => logger.error('Error setting sink', error));
|
||||
&& this._audioElementImpl.setSinkId(sinkId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -266,12 +266,9 @@ function _getAllParticipants(stateful) {
|
||||
*
|
||||
* @param {Object|Function} stateful - Object or function that can be resolved
|
||||
* to the Redux state.
|
||||
* @param {?boolean} ignoreToken - When true we ignore the token check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLocalParticipantModerator(
|
||||
stateful: Object | Function,
|
||||
ignoreToken: ?boolean = false) {
|
||||
export function isLocalParticipantModerator(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
@@ -281,8 +278,7 @@ export function isLocalParticipantModerator(
|
||||
|
||||
return (
|
||||
localParticipant.role === PARTICIPANT_ROLE.MODERATOR
|
||||
&& (ignoreToken
|
||||
|| !state['features/base/config'].enableUserRolesBasedOnToken
|
||||
&& (!state['features/base/config'].enableUserRolesBasedOnToken
|
||||
|| !state['features/base/jwt'].isGuest));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../i18n';
|
||||
|
||||
import BackButton from './BackButton';
|
||||
import ForwardButton from './ForwardButton';
|
||||
import Header from './Header';
|
||||
import HeaderLabel from './HeaderLabel';
|
||||
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Boolean to set the forward button disabled.
|
||||
*/
|
||||
forwardDisabled: boolean,
|
||||
|
||||
/**
|
||||
* The i18n key of the the forward button label.
|
||||
*/
|
||||
forwardLabelKey: ?string,
|
||||
|
||||
/**
|
||||
* The i18n key of the header label (title)
|
||||
*/
|
||||
headerLabelKey: ?string,
|
||||
|
||||
/**
|
||||
* Callback to be invoked on pressing the back button.
|
||||
*/
|
||||
onPressBack: ?Function,
|
||||
|
||||
/**
|
||||
* Callback to be invoked on pressing the forward button.
|
||||
*/
|
||||
onPressForward: ?Function,
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a header with the standard navigation content.
|
||||
*/
|
||||
class HeaderWithNavigation extends Component<Props> {
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { onPressBack, onPressForward } = this.props;
|
||||
|
||||
return (
|
||||
<Header>
|
||||
{ onPressBack && <BackButton onPress = { onPressBack } /> }
|
||||
<HeaderLabel labelKey = { this.props.headerLabelKey } />
|
||||
{ onPressForward && <ForwardButton
|
||||
disabled = { this.props.forwardDisabled }
|
||||
labelKey = { this.props.forwardLabelKey }
|
||||
onPress = { onPressForward } /> }
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(HeaderWithNavigation);
|
||||
@@ -5,11 +5,12 @@ import Swipeout from 'react-native-swipeout';
|
||||
|
||||
import { ColorPalette } from '../../../styles';
|
||||
|
||||
import Container from './Container';
|
||||
import Text from './Text';
|
||||
import styles from './styles';
|
||||
import type { Item } from '../../Types';
|
||||
|
||||
import AvatarListItem from './AvatarListItem';
|
||||
import Text from './Text';
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -92,6 +93,24 @@ export default class NavigateSectionListItem extends Component<Props> {
|
||||
return lines && lines.length ? lines.map(this._renderItemLine) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the secondary action label.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderSecondaryAction() {
|
||||
const { secondaryAction } = this.props;
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { secondaryAction }
|
||||
style = { styles.secondaryActionContainer }>
|
||||
<Text style = { styles.secondaryActionLabel }>+</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
@@ -119,12 +138,14 @@ export default class NavigateSectionListItem extends Component<Props> {
|
||||
|
||||
return (
|
||||
<Swipeout
|
||||
autoClose = { true }
|
||||
backgroundColor = { ColorPalette.transparent }
|
||||
right = { right }>
|
||||
<AvatarListItem
|
||||
item = { item }
|
||||
onPress = { this.props.onPress } />
|
||||
onPress = { this.props.onPress }>
|
||||
{ this.props.secondaryAction
|
||||
&& this._renderSecondaryAction() }
|
||||
</AvatarListItem>
|
||||
</Swipeout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ export { default as Container } from './Container';
|
||||
export { default as ForwardButton } from './ForwardButton';
|
||||
export { default as Header } from './Header';
|
||||
export { default as HeaderLabel } from './HeaderLabel';
|
||||
export { default as HeaderWithNavigation } from './HeaderWithNavigation';
|
||||
export { default as Image } from './Image';
|
||||
export { default as Link } from './Link';
|
||||
export { default as LoadingIndicator } from './LoadingIndicator';
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles';
|
||||
|
||||
const AVATAR_OPACITY = 0.4;
|
||||
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
|
||||
const SECONDARY_ACTION_BUTTON_SIZE = 30;
|
||||
|
||||
export const AVATAR_SIZE = 65;
|
||||
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
|
||||
@@ -217,6 +218,21 @@ const SECTION_LIST_STYLES = {
|
||||
color: OVERLAY_FONT_COLOR
|
||||
},
|
||||
|
||||
secondaryActionContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: ColorPalette.blue,
|
||||
borderRadius: 3,
|
||||
height: SECONDARY_ACTION_BUTTON_SIZE,
|
||||
justifyContent: 'center',
|
||||
margin: BoxModel.margin * 0.5,
|
||||
marginRight: BoxModel.margin,
|
||||
width: SECONDARY_ACTION_BUTTON_SIZE
|
||||
},
|
||||
|
||||
secondaryActionLabel: {
|
||||
color: ColorPalette.white
|
||||
},
|
||||
|
||||
touchableView: {
|
||||
flexDirection: 'row'
|
||||
}
|
||||
|
||||
86
react/features/base/session/Session.js
Normal file
86
react/features/base/session/Session.js
Normal file
@@ -0,0 +1,86 @@
|
||||
// @flow
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { toURLString } from '../util';
|
||||
|
||||
import type JitsiConference from 'lib-jitsi-meet/JitsiConference';
|
||||
import type JitsiConnection from 'lib-jitsi-meet/JitsiConnection';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*/
|
||||
export class Session {
|
||||
_conference: ?JitsiConference;
|
||||
_connection: ?JitsiConnection;
|
||||
conferenceFailed: boolean;
|
||||
id: string;
|
||||
locationURL: URL;
|
||||
room: string;
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {URL} locationURL - FIXME.
|
||||
* @param {string} room - FIXME.
|
||||
*/
|
||||
constructor(locationURL: URL, room: string) {
|
||||
this.locationURL = locationURL;
|
||||
this.room = room;
|
||||
this.id = uuid.v4().toUpperCase();
|
||||
this.conferenceFailed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {JitsiConference} [conference] - FIXME.
|
||||
*/
|
||||
set conference(conference: ?JitsiConference) {
|
||||
if (this._conference && conference && this._conference !== conference) {
|
||||
throw new Error(`Attempt to reassign conference to ${this.toString()}`);
|
||||
}
|
||||
|
||||
this._conference = conference;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @returns {?JitsiConference}
|
||||
*/
|
||||
get conference(): ?JitsiConference {
|
||||
return this._conference;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {JitsiConnection} [connection] - FIXME.
|
||||
*/
|
||||
set connection(connection: ?JitsiConnection) {
|
||||
if (this._connection && connection && this._connection !== connection) {
|
||||
throw new Error(`Attempt to reassign connection to ${this.toString()}`);
|
||||
}
|
||||
|
||||
this._connection = connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @returns {?JitsiConnection}
|
||||
*/
|
||||
get connection() {
|
||||
return this._connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
toString() {
|
||||
return `Session[id=${this.id}, URL: ${toURLString(this.locationURL)} room: ${this.room}]`;
|
||||
}
|
||||
}
|
||||
8
react/features/base/session/actionTypes.js
Normal file
8
react/features/base/session/actionTypes.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
export const SESSION_CREATED = 'SESSION_CREATED';
|
||||
|
||||
export const SESSION_STARTED = 'SESSION_STARTED';
|
||||
|
||||
export const SESSION_TERMINATED = 'SESSION_TERMINATED';
|
||||
|
||||
export const SESSION_FAILED = 'SESSION_FAILED';
|
||||
135
react/features/base/session/actions.js
Normal file
135
react/features/base/session/actions.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// @flow
|
||||
|
||||
import { conferenceLeft, conferenceWillLeave } from '../conference';
|
||||
|
||||
import { SESSION_CREATED, SESSION_FAILED, SESSION_STARTED, SESSION_TERMINATED } from './actionTypes';
|
||||
import { Session } from './Session';
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function endAllSessions() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const sessions = getState()['features/base/session'];
|
||||
|
||||
for (const session of sessions.values()) {
|
||||
dispatch(endSession(session));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Session} session - FIXME.
|
||||
* @returns {function(*): *}
|
||||
*/
|
||||
export function endSession(session: Session) {
|
||||
return (dispatch: Dispatch<any>) => {
|
||||
// The conference we have already joined or are joining.
|
||||
const conference_ = session.conference;
|
||||
|
||||
// Promise which completes when the conference has been left and the connection has been disconnected.
|
||||
let promise;
|
||||
|
||||
// Leave the conference.
|
||||
if (conference_) {
|
||||
// In a fashion similar to JitsiConference's CONFERENCE_LEFT event (and the respective Redux action) which
|
||||
// is fired after the conference has been left, notify the application about the intention to leave
|
||||
// the conference.
|
||||
dispatch(conferenceWillLeave(conference_));
|
||||
|
||||
promise
|
||||
= conference_.leave()
|
||||
.catch(error => {
|
||||
logger.warn('JitsiConference.leave() rejected with:', error);
|
||||
|
||||
// The library lib-jitsi-meet failed to make the JitsiConference leave. Which may be because
|
||||
// JitsiConference thinks it has already left. Regardless of the failure reason, continue in
|
||||
// jitsi-meet as if the leave has succeeded.
|
||||
dispatch(conferenceLeft(conference_));
|
||||
});
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
const connection_ = session.connection;
|
||||
|
||||
if (connection_) {
|
||||
promise = promise.then(() => connection_.disconnect());
|
||||
}
|
||||
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {URL} locationURL - FIXME.
|
||||
* @param {string} room - FIXME.
|
||||
* @returns {{
|
||||
* type: SESSION_CREATED,
|
||||
* session: Session
|
||||
* }}
|
||||
*/
|
||||
export function createSession(locationURL: URL, room: string) {
|
||||
return {
|
||||
type: SESSION_CREATED,
|
||||
session: new Session(locationURL, room)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Session} session - FIXME.
|
||||
* @returns {{
|
||||
* type: SESSION_FAILED,
|
||||
* session: Session
|
||||
* }}
|
||||
*/
|
||||
export function sessionFailed(session: Session) {
|
||||
return {
|
||||
type: SESSION_FAILED,
|
||||
session
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Session} session - FIXME.
|
||||
* @returns {{
|
||||
* session: Session,
|
||||
* type: string
|
||||
* }}
|
||||
*/
|
||||
export function sessionStarted(session: Session) {
|
||||
return {
|
||||
type: SESSION_STARTED,
|
||||
session
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Session} session - FIXME.
|
||||
* @returns {{
|
||||
* type: SESSION_TERMINATED,
|
||||
* session: Session
|
||||
* }}
|
||||
*/
|
||||
export function sessionTerminated(session: Session) {
|
||||
return {
|
||||
type: SESSION_TERMINATED,
|
||||
session
|
||||
};
|
||||
}
|
||||
6
react/features/base/session/index.js
Normal file
6
react/features/base/session/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './selectors';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
164
react/features/base/session/middleware.js
Normal file
164
react/features/base/session/middleware.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { APP_WILL_NAVIGATE } from '../app';
|
||||
import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_LEFT, CONFERENCE_WILL_JOIN, SET_ROOM } from '../conference';
|
||||
import { LOAD_CONFIG_ERROR } from '../config';
|
||||
import { connect, CONNECTION_DISCONNECTED, CONNECTION_FAILED, CONNECTION_WILL_CONNECT } from '../connection';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { createDesiredLocalTracks } from '../tracks';
|
||||
import { toURLString } from '../util';
|
||||
|
||||
import { createSession, endAllSessions, endSession, sessionFailed, sessionStarted, sessionTerminated } from './actions';
|
||||
import { SESSION_CREATED, SESSION_FAILED, SESSION_STARTED, SESSION_TERMINATED } from './actionTypes';
|
||||
import { findSessionForConference, findSessionForConnection, findSessionForLocationURL } from './selectors';
|
||||
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case APP_WILL_NAVIGATE: {
|
||||
const { locationURL, room } = action;
|
||||
|
||||
// Currently only one session is allowed at a time
|
||||
dispatch(endAllSessions());
|
||||
|
||||
// Start a new session if there's a conference room name defined
|
||||
room && room.length && dispatch(createSession(locationURL, room));
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_FAILED: {
|
||||
const { conference, error } = action;
|
||||
const { recoverable } = error;
|
||||
const session = findSessionForConference(getState(), conference);
|
||||
|
||||
if (session) {
|
||||
session.conference = null;
|
||||
if (typeof recoverable === 'undefined' || recoverable === false) {
|
||||
session.conferenceFailed = true;
|
||||
if (session.connection) {
|
||||
// The sessionFailed is expected to be dispatched from either CONNECTION_DISCONNECTED
|
||||
// or CONNECTION_FAILED handler in this middleware. The purpose is to delay the SESSION_FAILED until
|
||||
// the XMPP connection is properly disposed.
|
||||
dispatch(endSession(session));
|
||||
} else {
|
||||
dispatch(sessionFailed(session));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('CONFERENCE_FAILED - no session found');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_LEFT: {
|
||||
const { conference } = action;
|
||||
const session = findSessionForConference(getState(), conference);
|
||||
|
||||
if (session) {
|
||||
session.conference = null;
|
||||
|
||||
// If there's any existing connection wait for it to be closed first
|
||||
session.connection || dispatch(sessionTerminated(session));
|
||||
} else {
|
||||
console.error('CONFERENCE_LEFT - no session found');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_JOINED: {
|
||||
const { conference } = action;
|
||||
const session = findSessionForConference(getState(), conference);
|
||||
|
||||
if (session) {
|
||||
dispatch(sessionStarted(session));
|
||||
} else {
|
||||
console.error('CONFERENCE_JOINED - no session found');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_WILL_JOIN: {
|
||||
const { conference } = action;
|
||||
const { connection } = conference;
|
||||
|
||||
const session = findSessionForConnection(getState(), connection);
|
||||
|
||||
if (session) {
|
||||
session.conference = conference;
|
||||
} else {
|
||||
console.error('CONFERENCE_WILL_JOIN - no session found');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LOAD_CONFIG_ERROR: {
|
||||
const { locationURL, room } = action;
|
||||
|
||||
// There won't be a session if there's no room (it happen when the config is loaded for the welcome page).
|
||||
if (room) {
|
||||
const session = findSessionForLocationURL(getState(), locationURL);
|
||||
|
||||
if (session) {
|
||||
dispatch(sessionFailed(session));
|
||||
} else {
|
||||
console.error(`LOAD_CONFIG_ERROR - no session found for: ${toURLString(locationURL)}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONNECTION_DISCONNECTED: {
|
||||
const { connection } = action;
|
||||
const session = findSessionForConnection(getState(), connection);
|
||||
|
||||
if (session) {
|
||||
session.connection = null;
|
||||
dispatch(
|
||||
session.conferenceFailed ? sessionFailed(session) : sessionTerminated(session));
|
||||
} else {
|
||||
console.error('CONNECTION_DISCONNECTED - no session found');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONNECTION_FAILED: {
|
||||
const { connection, error } = action;
|
||||
const { recoverable } = error;
|
||||
const session = findSessionForConnection(getState(), connection);
|
||||
|
||||
if (session) {
|
||||
session.connection = null;
|
||||
if (typeof recoverable === 'undefined' || recoverable === false) {
|
||||
dispatch(sessionFailed(session));
|
||||
}
|
||||
} else {
|
||||
console.error('CONNECTION_FAILED - no session found');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONNECTION_WILL_CONNECT: {
|
||||
const { connection, locationURL } = action;
|
||||
const session = findSessionForLocationURL(getState(), locationURL);
|
||||
|
||||
if (session) {
|
||||
session.connection = connection;
|
||||
} else {
|
||||
console.error(`CONNECTION_WILL_CONNECT - no session found for: ${toURLString(locationURL)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SET_ROOM: {
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
const session = findSessionForLocationURL(getState(), locationURL);
|
||||
|
||||
// Web has different logic for creating the local tracks and starting the connection
|
||||
if (session && typeof APP === 'undefined') {
|
||||
dispatch(createDesiredLocalTracks());
|
||||
dispatch(connect());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SESSION_CREATED:
|
||||
case SESSION_STARTED:
|
||||
case SESSION_FAILED:
|
||||
case SESSION_TERMINATED:
|
||||
console.info(`DEBUG ${action.type} ${action.session}`);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
27
react/features/base/session/reducer.js
Normal file
27
react/features/base/session/reducer.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ReducerRegistry } from '../redux';
|
||||
|
||||
import { SESSION_CREATED, SESSION_FAILED, SESSION_TERMINATED } from './actionTypes';
|
||||
|
||||
ReducerRegistry.register('features/base/session', (state = new Map(), action) => {
|
||||
switch (action.type) {
|
||||
case SESSION_CREATED: {
|
||||
const { session } = action;
|
||||
const nextMap = new Map(state);
|
||||
|
||||
nextMap.set(session.id, session);
|
||||
|
||||
return nextMap;
|
||||
}
|
||||
case SESSION_TERMINATED:
|
||||
case SESSION_FAILED: {
|
||||
const { session } = action;
|
||||
const nextMap = new Map(state);
|
||||
|
||||
nextMap.delete(session.id);
|
||||
|
||||
return nextMap;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
56
react/features/base/session/selectors.js
Normal file
56
react/features/base/session/selectors.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object} state - FIXME.
|
||||
* @param {JitsiConnection} connection - FIXME.
|
||||
* @returns {Session|null}
|
||||
*/
|
||||
export function findSessionForConnection(state, connection) {
|
||||
const sessions = state['features/base/session'];
|
||||
|
||||
for (const session of sessions.values()) {
|
||||
if (session.connection === connection) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object} state - FIXME.
|
||||
* @param {JitsiConference} conference - FIXME.
|
||||
* @returns {Session|null}
|
||||
*/
|
||||
export function findSessionForConference(state, conference) {
|
||||
const sessions = state['features/base/session'];
|
||||
|
||||
for (const session of sessions.values()) {
|
||||
if (session.conference === conference) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object} state - FIXME.
|
||||
* @param {URL} locationURL - FIXME.
|
||||
* @returns {Session|null}
|
||||
*/
|
||||
export function findSessionForLocationURL(state, locationURL) {
|
||||
const sessions = state['features/base/session'];
|
||||
|
||||
for (const session of sessions.values()) {
|
||||
if (session.locationURL === locationURL) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -97,152 +97,3 @@ export function getServerURL(stateful: Object | Function) {
|
||||
|
||||
return state['features/base/settings'].serverURL || DEFAULT_SERVER_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches known devices for a matching deviceId and fall back to matching on
|
||||
* label. Returns the stored preferred cameraDeviceId if a match is not found.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux state object or
|
||||
* {@code getState} function.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getUserSelectedCameraDeviceId(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
userSelectedCameraDeviceId,
|
||||
userSelectedCameraDeviceLabel
|
||||
} = state['features/base/settings'];
|
||||
const { videoInput } = state['features/base/devices'].availableDevices;
|
||||
|
||||
return _getUserSelectedDeviceId({
|
||||
availableDevices: videoInput,
|
||||
|
||||
// Operating systems may append " #{number}" somewhere in the label so
|
||||
// find and strip that bit.
|
||||
matchRegex: /\s#\d*(?!.*\s#\d*)/,
|
||||
userSelectedDeviceId: userSelectedCameraDeviceId,
|
||||
userSelectedDeviceLabel: userSelectedCameraDeviceLabel,
|
||||
replacement: ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches known devices for a matching deviceId and fall back to matching on
|
||||
* label. Returns the stored preferred micDeviceId if a match is not found.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux state object or
|
||||
* {@code getState} function.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getUserSelectedMicDeviceId(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
userSelectedMicDeviceId,
|
||||
userSelectedMicDeviceLabel
|
||||
} = state['features/base/settings'];
|
||||
const { audioInput } = state['features/base/devices'].availableDevices;
|
||||
|
||||
return _getUserSelectedDeviceId({
|
||||
availableDevices: audioInput,
|
||||
|
||||
// Operating systems may append " ({number}-" somewhere in the label so
|
||||
// find and strip that bit.
|
||||
matchRegex: /\s\(\d*-\s(?!.*\s\(\d*-\s)/,
|
||||
userSelectedDeviceId: userSelectedMicDeviceId,
|
||||
userSelectedDeviceLabel: userSelectedMicDeviceLabel,
|
||||
replacement: ' ('
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches known devices for a matching deviceId and fall back to matching on
|
||||
* label. Returns the stored preferred audioOutputDeviceId if a match is not found.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux state object or
|
||||
* {@code getState} function.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getUserSelectedOutputDeviceId(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
userSelectedAudioOutputDeviceId,
|
||||
userSelectedAudioOutputDeviceLabel
|
||||
} = state['features/base/settings'];
|
||||
const { audioOutput } = state['features/base/devices'].availableDevices;
|
||||
|
||||
return _getUserSelectedDeviceId({
|
||||
availableDevices: audioOutput,
|
||||
matchRegex: undefined,
|
||||
userSelectedDeviceId: userSelectedAudioOutputDeviceId,
|
||||
userSelectedDeviceLabel: userSelectedAudioOutputDeviceLabel,
|
||||
replacement: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to abstract the logic for choosing which device ID to
|
||||
* use. Falls back to fuzzy matching on label if a device ID match is not found.
|
||||
*
|
||||
* @param {Object} options - The arguments used to match find the preferred
|
||||
* device ID from available devices.
|
||||
* @param {Array<string>} options.availableDevices - The array of currently
|
||||
* available devices to match against.
|
||||
* @param {Object} options.matchRegex - The regex to use to find strings
|
||||
* appended to the label by the operating system. The matches will be replaced
|
||||
* with options.replacement, with the intent of matching the same device that
|
||||
* might have a modified label.
|
||||
* @param {string} options.userSelectedDeviceId - The device ID the participant
|
||||
* prefers to use.
|
||||
* @param {string} options.userSelectedDeviceLabel - The label associated with the
|
||||
* device ID the participant prefers to use.
|
||||
* @param {string} options.replacement - The string to use with
|
||||
* options.matchRegex to remove identifies added to the label by the operating
|
||||
* system.
|
||||
* @private
|
||||
* @returns {string} The preferred device ID to use for media.
|
||||
*/
|
||||
function _getUserSelectedDeviceId(options) {
|
||||
const {
|
||||
availableDevices,
|
||||
matchRegex,
|
||||
userSelectedDeviceId,
|
||||
userSelectedDeviceLabel,
|
||||
replacement
|
||||
} = options;
|
||||
|
||||
// If there is no label at all, there is no need to fall back to checking
|
||||
// the label for a fuzzy match.
|
||||
if (!userSelectedDeviceLabel || !userSelectedDeviceId) {
|
||||
return userSelectedDeviceId;
|
||||
}
|
||||
|
||||
const foundMatchingBasedonDeviceId = availableDevices.find(
|
||||
candidate => candidate.deviceId === userSelectedDeviceId);
|
||||
|
||||
// Prioritize matching the deviceId
|
||||
if (foundMatchingBasedonDeviceId) {
|
||||
return userSelectedDeviceId;
|
||||
}
|
||||
|
||||
const strippedDeviceLabel
|
||||
= matchRegex ? userSelectedDeviceLabel.replace(matchRegex, replacement)
|
||||
: userSelectedDeviceLabel;
|
||||
const foundMatchBasedOnLabel = availableDevices.find(candidate => {
|
||||
const { label } = candidate;
|
||||
|
||||
if (!label) {
|
||||
return false;
|
||||
} else if (strippedDeviceLabel === label) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const strippedCandidateLabel
|
||||
= label.replace(matchRegex, replacement);
|
||||
|
||||
return strippedDeviceLabel === strippedCandidateLabel;
|
||||
});
|
||||
|
||||
return foundMatchBasedOnLabel
|
||||
? foundMatchBasedOnLabel.deviceId : userSelectedDeviceId;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,7 @@ const DEFAULT_STATE = {
|
||||
startWithVideoMuted: false,
|
||||
userSelectedAudioOutputDeviceId: undefined,
|
||||
userSelectedCameraDeviceId: undefined,
|
||||
userSelectedMicDeviceId: undefined,
|
||||
userSelectedAudioOutputDeviceLabel: undefined,
|
||||
userSelectedCameraDeviceLabel: undefined,
|
||||
userSelectedMicDeviceLabel: undefined
|
||||
userSelectedMicDeviceId: undefined
|
||||
};
|
||||
|
||||
const STORE_NAME = 'features/base/settings';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import { AsyncStorage } from 'react-native';
|
||||
|
||||
/**
|
||||
* A Web Sorage API implementation used for polyfilling
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
import JitsiMeetJS, { JitsiTrackErrors, JitsiTrackEvents }
|
||||
from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../media';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
getUserSelectedMicDeviceId
|
||||
} from '../settings';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -41,13 +37,13 @@ export function createLocalTracksF(
|
||||
// reliance on the global variable APP will go away.
|
||||
store || (store = APP.store); // eslint-disable-line no-param-reassign
|
||||
|
||||
const state = store.getState();
|
||||
const settings = store.getState()['features/base/settings'];
|
||||
|
||||
if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
|
||||
cameraDeviceId = getUserSelectedCameraDeviceId(state);
|
||||
cameraDeviceId = settings.userSelectedCameraDeviceId;
|
||||
}
|
||||
if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
|
||||
micDeviceId = getUserSelectedMicDeviceId(state);
|
||||
micDeviceId = settings.userSelectedMicDeviceId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,8 @@
|
||||
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { getLocalizedDateFormatter } from '../../base/i18n';
|
||||
import { getAvatarURLByParticipantId } from '../../base/participants';
|
||||
|
||||
/**
|
||||
* Formatter string to display the message timestamp.
|
||||
*/
|
||||
const TIMESTAMP_FORMAT = 'H:mm';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@code AbstractChatMessage}.
|
||||
*/
|
||||
@@ -25,24 +19,6 @@ export type Props = {
|
||||
*/
|
||||
message: Object,
|
||||
|
||||
/**
|
||||
* Whether or not the avatar image of the participant which sent the message
|
||||
* should be displayed.
|
||||
*/
|
||||
showAvatar: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the name of the participant which sent the message should
|
||||
* be displayed.
|
||||
*/
|
||||
showDisplayName: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the time at which the message was sent should be
|
||||
* displayed.
|
||||
*/
|
||||
showTimestamp: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to receive translated strings.
|
||||
*/
|
||||
@@ -52,17 +28,7 @@ export type Props = {
|
||||
/**
|
||||
* Abstract component to display a chat message.
|
||||
*/
|
||||
export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
|
||||
/**
|
||||
* Returns the timestamp to display for the message.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getFormattedTimestamp() {
|
||||
return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
|
||||
.format(TIMESTAMP_FORMAT);
|
||||
}
|
||||
}
|
||||
export default class AbstractChatMessage<P: Props> extends PureComponent<P> {}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The messages array to render.
|
||||
*/
|
||||
messages: Array<Object>
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract component to display a list of chat messages, grouped by sender.
|
||||
*
|
||||
* @extends PureComponent
|
||||
*/
|
||||
export default class AbstractMessageContainer extends PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
messages: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterates over all the messages and creates nested arrays which hold
|
||||
* consecutive messages sent by the same participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<Array<Object>>}
|
||||
*/
|
||||
_getMessagesGroupedBySender() {
|
||||
const messagesCount = this.props.messages.length;
|
||||
const groups = [];
|
||||
let currentGrouping = [];
|
||||
let currentGroupParticipantId;
|
||||
|
||||
for (let i = 0; i < messagesCount; i++) {
|
||||
const message = this.props.messages[i];
|
||||
|
||||
if (message.id === currentGroupParticipantId) {
|
||||
currentGrouping.push(message);
|
||||
} else {
|
||||
currentGrouping.length && groups.push(currentGrouping);
|
||||
|
||||
currentGrouping = [ message ];
|
||||
currentGroupParticipantId = message.id;
|
||||
}
|
||||
}
|
||||
|
||||
groups.push(currentGrouping);
|
||||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,12 @@ import { KeyboardAvoidingView, SafeAreaView } from 'react-native';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import { HeaderWithNavigation, SlidingView } from '../../../base/react';
|
||||
import {
|
||||
BackButton,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
SlidingView
|
||||
} from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import AbstractChat, {
|
||||
@@ -36,9 +41,10 @@ class Chat extends AbstractChat<Props> {
|
||||
<KeyboardAvoidingView
|
||||
behavior = 'padding'
|
||||
style = { styles.chatContainer }>
|
||||
<HeaderWithNavigation
|
||||
headerLabelKey = 'chat.title'
|
||||
onPressBack = { this.props._onToggleChat } />
|
||||
<Header>
|
||||
<BackButton onPress = { this.props._onToggleChat } />
|
||||
<HeaderLabel labelKey = 'chat.title' />
|
||||
</Header>
|
||||
<SafeAreaView style = { styles.backdrop }>
|
||||
<MessageContainer messages = { this.props._messages } />
|
||||
<ChatInputBar onSend = { this.props._onSendMessage } />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { getLocalizedDateFormatter, translate } from '../../../base/i18n';
|
||||
import { Avatar } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
@@ -13,6 +13,16 @@ import AbstractChatMessage, {
|
||||
} from '../AbstractChatMessage';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Size of the rendered avatar in the message.
|
||||
*/
|
||||
const AVATAR_SIZE = 32;
|
||||
|
||||
/**
|
||||
* Formatter string to display the message timestamp.
|
||||
*/
|
||||
const TIMESTAMP_FORMAT = 'H:mm';
|
||||
|
||||
/**
|
||||
* Renders a single chat message.
|
||||
*/
|
||||
@@ -24,6 +34,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||
*/
|
||||
render() {
|
||||
const { message } = this.props;
|
||||
const timeStamp = getLocalizedDateFormatter(
|
||||
new Date(message.timestamp)).format(TIMESTAMP_FORMAT);
|
||||
const localMessage = message.messageType === 'local';
|
||||
|
||||
// Style arrays that need to be updated in various scenarios, such as
|
||||
@@ -48,12 +60,18 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||
|
||||
return (
|
||||
<View style = { styles.messageWrapper } >
|
||||
{ this._renderAvatar() }
|
||||
{
|
||||
|
||||
// Avatar is only rendered for remote messages.
|
||||
!localMessage && this._renderAvatar()
|
||||
}
|
||||
<View style = { detailsWrapperStyle }>
|
||||
<View style = { textWrapperStyle } >
|
||||
{
|
||||
this.props.showDisplayName
|
||||
&& this._renderDisplayName()
|
||||
|
||||
// Display name is only rendered for remote
|
||||
// messages.
|
||||
!localMessage && this._renderDisplayName()
|
||||
}
|
||||
<Text style = { styles.messageText }>
|
||||
{ message.messageType === 'error'
|
||||
@@ -64,26 +82,27 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||
: message.message }
|
||||
</Text>
|
||||
</View>
|
||||
{ this.props.showTimestamp && this._renderTimestamp() }
|
||||
<Text style = { styles.timeText }>
|
||||
{ timeStamp }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_getFormattedTimestamp: () => string;
|
||||
|
||||
/**
|
||||
* Renders the avatar of the sender.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderAvatar() {
|
||||
const { _avatarURL } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.avatarWrapper }>
|
||||
{ this.props.showAvatar && <Avatar
|
||||
size = { styles.avatarWrapper.width }
|
||||
uri = { this.props._avatarURL } />
|
||||
}
|
||||
<Avatar
|
||||
size = { AVATAR_SIZE }
|
||||
uri = { _avatarURL } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -94,22 +113,11 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderDisplayName() {
|
||||
const { message } = this.props;
|
||||
|
||||
return (
|
||||
<Text style = { styles.displayName }>
|
||||
{ this.props.message.displayName }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the time at which the message was sent.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderTimestamp() {
|
||||
return (
|
||||
<Text style = { styles.timeText }>
|
||||
{ this._getFormattedTimestamp() }
|
||||
{ message.displayName }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { FlatList } from 'react-native';
|
||||
|
||||
import ChatMessage from './ChatMessage';
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The messages array to render.
|
||||
*/
|
||||
messages: Array<Object>
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a container to render all the chat messages in a conference.
|
||||
*/
|
||||
export default class ChatMessageGroup extends Component<Props> {
|
||||
/**
|
||||
* Instantiates a new instance of the component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._keyExtractor = this._keyExtractor.bind(this);
|
||||
this._renderMessage = this._renderMessage.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<FlatList
|
||||
data = { this.props.messages }
|
||||
inverted = { true }
|
||||
keyExtractor = { this._keyExtractor }
|
||||
renderItem = { this._renderMessage }
|
||||
style = { styles.messageContainer } />
|
||||
);
|
||||
}
|
||||
|
||||
_keyExtractor: Object => string
|
||||
|
||||
/**
|
||||
* Key extractor for the flatlist.
|
||||
*
|
||||
* @param {Object} item - The flatlist item that we need the key to be
|
||||
* generated for.
|
||||
* @param {number} index - The index of the element.
|
||||
* @returns {string}
|
||||
*/
|
||||
_keyExtractor(item, index) {
|
||||
return `key_${index}`;
|
||||
}
|
||||
|
||||
_renderMessage: Object => React$Element<*>;
|
||||
|
||||
/**
|
||||
* Renders a single chat message.
|
||||
*
|
||||
* @param {Object} message - The chat message to render.
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderMessage({ index, item: message }) {
|
||||
return (
|
||||
<ChatMessage
|
||||
message = { message }
|
||||
showAvatar = {
|
||||
this.props.messages[0].messageType !== 'local'
|
||||
&& index === this.props.messages.length - 1
|
||||
}
|
||||
showDisplayName = {
|
||||
this.props.messages[0].messageType === 'remote'
|
||||
&& index === this.props.messages.length - 1
|
||||
}
|
||||
showTimestamp = { index === 0 } />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { FlatList } from 'react-native';
|
||||
|
||||
import AbstractMessageContainer, { type Props }
|
||||
from '../AbstractMessageContainer';
|
||||
|
||||
import ChatMessageGroup from './ChatMessageGroup';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The messages array to render.
|
||||
*/
|
||||
messages: Array<Object>
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a container to render all the chat messages in a conference.
|
||||
*/
|
||||
export default class MessageContainer extends AbstractMessageContainer {
|
||||
export default class MessageContainer extends Component<Props> {
|
||||
/**
|
||||
* Instantiates a new instance of the component.
|
||||
*
|
||||
@@ -22,7 +27,7 @@ export default class MessageContainer extends AbstractMessageContainer {
|
||||
super(props);
|
||||
|
||||
this._keyExtractor = this._keyExtractor.bind(this);
|
||||
this._renderMessageGroup = this._renderMessageGroup.bind(this);
|
||||
this._renderMessage = this._renderMessage.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,16 +38,14 @@ export default class MessageContainer extends AbstractMessageContainer {
|
||||
render() {
|
||||
return (
|
||||
<FlatList
|
||||
data = { this._getMessagesGroupedBySender() }
|
||||
data = { this.props.messages }
|
||||
inverted = { true }
|
||||
keyExtractor = { this._keyExtractor }
|
||||
renderItem = { this._renderMessageGroup }
|
||||
renderItem = { this._renderMessage }
|
||||
style = { styles.messageContainer } />
|
||||
);
|
||||
}
|
||||
|
||||
_getMessagesGroupedBySender: () => Array<Array<Object>>;
|
||||
|
||||
_keyExtractor: Object => string
|
||||
|
||||
/**
|
||||
@@ -57,15 +60,17 @@ export default class MessageContainer extends AbstractMessageContainer {
|
||||
return `key_${index}`;
|
||||
}
|
||||
|
||||
_renderMessageGroup: Object => React$Element<*>;
|
||||
_renderMessage: Object => React$Element<*>;
|
||||
|
||||
/**
|
||||
* Renders a single chat message.
|
||||
*
|
||||
* @param {Array<Object>} messages - The chat message to render.
|
||||
* @param {Object} message - The chat message to render.
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderMessageGroup({ item: messages }) {
|
||||
return <ChatMessageGroup messages = { messages } />;
|
||||
_renderMessage({ item: message }) {
|
||||
return (
|
||||
<ChatMessage message = { message } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ export default {
|
||||
* Wrapper View for the avatar.
|
||||
*/
|
||||
avatarWrapper: {
|
||||
marginRight: 8,
|
||||
width: 32
|
||||
marginRight: 8
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,8 +12,8 @@ import AbstractChat, {
|
||||
type Props
|
||||
} from '../AbstractChat';
|
||||
import ChatInput from './ChatInput';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import DisplayNameForm from './DisplayNameForm';
|
||||
import MessageContainer from './MessageContainer';
|
||||
|
||||
/**
|
||||
* React Component for holding the chat feature in a side panel that slides in
|
||||
@@ -28,10 +28,10 @@ class Chat extends AbstractChat<Props> {
|
||||
_isExited: boolean;
|
||||
|
||||
/**
|
||||
* Reference to the React Component for displaying chat messages. Used for
|
||||
* scrolling to the end of the chat messages.
|
||||
* Reference to the HTML element at the end of the list of displayed chat
|
||||
* messages. Used for scrolling to the end of the chat messages.
|
||||
*/
|
||||
_messageContainerRef: Object;
|
||||
_messagesListEnd: ?HTMLElement;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code Chat} instance.
|
||||
@@ -43,34 +43,32 @@ class Chat extends AbstractChat<Props> {
|
||||
super(props);
|
||||
|
||||
this._isExited = true;
|
||||
this._messageContainerRef = React.createRef();
|
||||
this._messagesListEnd = null;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._renderMessage = this._renderMessage.bind(this);
|
||||
this._renderPanelContent = this._renderPanelContent.bind(this);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onChatInputResize = this._onChatInputResize.bind(this);
|
||||
this._setMessageListEndRef = this._setMessageListEndRef.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidMount}.
|
||||
* Implements React's {@link Component#componentDidMount()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._scrollMessageContainerToBottom(true);
|
||||
this._scrollMessagesToBottom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidUpdate}.
|
||||
* Updates chat input focus.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props._messages !== prevProps._messages) {
|
||||
this._scrollMessageContainerToBottom(true);
|
||||
} else if (this.props._isOpen && !prevProps._isOpen) {
|
||||
this._scrollMessageContainerToBottom(false);
|
||||
this._scrollMessagesToBottom();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,19 +88,6 @@ class Chat extends AbstractChat<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
_onChatInputResize: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked when {@code ChatInput} changes height. Preserves
|
||||
* displaying the latest message if it is scrolled to.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChatInputResize() {
|
||||
this._messageContainerRef.current.maybeUpdateBottomScroll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a React Element for showing chat messages and a form to send new
|
||||
* chat messages.
|
||||
@@ -111,30 +96,38 @@ class Chat extends AbstractChat<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderChat() {
|
||||
const messages = this.props._messages.map(this._renderMessage);
|
||||
|
||||
messages.push(<div
|
||||
key = 'end-marker'
|
||||
ref = { this._setMessageListEndRef } />);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MessageContainer
|
||||
messages = { this.props._messages }
|
||||
ref = { this._messageContainerRef } />
|
||||
<ChatInput onResize = { this._onChatInputResize } />
|
||||
</>
|
||||
<div
|
||||
className = 'sideToolbarContainer__inner'
|
||||
id = 'chat_container'>
|
||||
<div id = 'chatconversation'>
|
||||
{ messages }
|
||||
</div>
|
||||
<ChatInput />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderMessage: (Object) => void;
|
||||
|
||||
/**
|
||||
* Instantiates a React Element to display at the top of {@code Chat} to
|
||||
* close {@code Chat}.
|
||||
* Called by {@code _onSubmitMessage} to create the chat div.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
* @param {string} message - The chat message to display.
|
||||
* @param {string} id - The chat message ID to use as a unique key.
|
||||
* @returns {Array<ReactElement>}
|
||||
*/
|
||||
_renderChatHeader() {
|
||||
_renderMessage(message: Object, id: string) {
|
||||
return (
|
||||
<div className = 'chat-header'>
|
||||
<div
|
||||
className = 'chat-close'
|
||||
onClick = { this.props._onToggleChat }>X</div>
|
||||
</div>
|
||||
<ChatMessage
|
||||
key = { id }
|
||||
message = { message } />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,15 +145,17 @@ class Chat extends AbstractChat<Props> {
|
||||
_renderPanelContent(state) {
|
||||
this._isExited = state === 'exited';
|
||||
|
||||
const { _isOpen, _showNamePrompt } = this.props;
|
||||
const { _isOpen, _onToggleChat, _showNamePrompt } = this.props;
|
||||
const ComponentToRender = !_isOpen && state === 'exited'
|
||||
? null
|
||||
: (
|
||||
<>
|
||||
{ this._renderChatHeader() }
|
||||
<div>
|
||||
<div
|
||||
className = 'chat-close'
|
||||
onClick = { _onToggleChat }>X</div>
|
||||
{ _showNamePrompt
|
||||
? <DisplayNameForm /> : this._renderChat() }
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
let className = '';
|
||||
|
||||
@@ -172,7 +167,7 @@ class Chat extends AbstractChat<Props> {
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { `sideToolbarContainer ${className}` }
|
||||
className = { className }
|
||||
id = 'sideToolbarContainer'>
|
||||
{ ComponentToRender }
|
||||
</div>
|
||||
@@ -180,18 +175,31 @@ class Chat extends AbstractChat<Props> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the chat messages so the latest message is visible.
|
||||
* Automatically scrolls the displayed chat messages down to the latest.
|
||||
*
|
||||
* @param {boolean} withAnimation - Whether or not to show a scrolling
|
||||
* animation.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_scrollMessageContainerToBottom(withAnimation) {
|
||||
if (this._messageContainerRef.current) {
|
||||
this._messageContainerRef.current.scrollToBottom(withAnimation);
|
||||
_scrollMessagesToBottom() {
|
||||
if (this._messagesListEnd) {
|
||||
this._messagesListEnd.scrollIntoView({
|
||||
behavior: this._isExited ? 'auto' : 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_setMessageListEndRef: (?HTMLElement) => void;
|
||||
|
||||
/**
|
||||
* Sets a reference to the HTML element at the bottom of the message list.
|
||||
*
|
||||
* @param {Object} messageListEnd - The HTML element.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setMessageListEndRef(messageListEnd: ?HTMLElement) {
|
||||
this._messagesListEnd = messageListEnd;
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import { sendMessage } from '../../actions';
|
||||
@@ -23,15 +21,9 @@ type Props = {
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Optional callback to invoke when the chat textarea has auto-resized to
|
||||
* fit overflowing text.
|
||||
* Optional callback to get a reference to the chat input element.
|
||||
*/
|
||||
onResize: ?Function,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
getChatInputRef?: Function
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -92,7 +84,7 @@ class ChatInput extends Component<Props, State> {
|
||||
* HTML Textareas do not support autofocus. Simulate autofocus by
|
||||
* manually focusing.
|
||||
*/
|
||||
this._focus();
|
||||
this.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,14 +113,13 @@ class ChatInput extends Component<Props, State> {
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'usrmsg-form'>
|
||||
<TextareaAutosize
|
||||
<textarea
|
||||
data-i18n = '[placeholder]chat.messagebox'
|
||||
id = 'usermsg'
|
||||
inputRef = { this._setTextAreaRef }
|
||||
maxRows = { 5 }
|
||||
onChange = { this._onMessageChange }
|
||||
onHeightChange = { this.props.onResize }
|
||||
onKeyDown = { this._onDetectSubmit }
|
||||
placeholder = { this.props.t('chat.messagebox') }
|
||||
placeholder = { 'Enter Text...' }
|
||||
ref = { this._setTextAreaRef }
|
||||
value = { this.state.message } />
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,12 +127,20 @@ class ChatInput extends Component<Props, State> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Place cursor focus on this component's text area.
|
||||
* Removes cursor focus on this component's text area.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_focus() {
|
||||
blur() {
|
||||
this._textArea && this._textArea.blur();
|
||||
}
|
||||
|
||||
/**
|
||||
* Place cursor focus on this component's text area.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
focus() {
|
||||
this._textArea && this._textArea.focus();
|
||||
}
|
||||
|
||||
@@ -199,7 +198,7 @@ class ChatInput extends Component<Props, State> {
|
||||
showSmileysPanel: false
|
||||
});
|
||||
|
||||
this._focus();
|
||||
this.focus();
|
||||
}
|
||||
|
||||
_onToggleSmileysPanel: () => void;
|
||||
@@ -213,7 +212,7 @@ class ChatInput extends Component<Props, State> {
|
||||
_onToggleSmileysPanel() {
|
||||
this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
|
||||
|
||||
this._focus();
|
||||
this.focus();
|
||||
}
|
||||
|
||||
_setTextAreaRef: (?HTMLTextAreaElement) => void;
|
||||
@@ -227,7 +226,11 @@ class ChatInput extends Component<Props, State> {
|
||||
*/
|
||||
_setTextAreaRef(textAreaElement: ?HTMLTextAreaElement) {
|
||||
this._textArea = textAreaElement;
|
||||
|
||||
if (this.props.getChatInputRef) {
|
||||
this.props.getChatInputRef(textAreaElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(ChatInput));
|
||||
export default connect()(ChatInput);
|
||||
|
||||
@@ -23,12 +23,24 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||
*/
|
||||
render() {
|
||||
const { message } = this.props;
|
||||
const messageToDisplay = message.messageType === 'error'
|
||||
? this.props.t('chat.error', {
|
||||
let messageTypeClassname = '';
|
||||
let messageToDisplay = message.message;
|
||||
|
||||
switch (message.messageType) {
|
||||
case 'local':
|
||||
messageTypeClassname = 'localuser';
|
||||
|
||||
break;
|
||||
case 'error':
|
||||
messageTypeClassname = 'error';
|
||||
messageToDisplay = this.props.t('chat.error', {
|
||||
error: message.error,
|
||||
originalText: message.message
|
||||
})
|
||||
: message.message;
|
||||
originalText: messageToDisplay
|
||||
});
|
||||
break;
|
||||
default:
|
||||
messageTypeClassname = 'remoteuser';
|
||||
}
|
||||
|
||||
// replace links and smileys
|
||||
// Strophe already escapes special symbols on sending,
|
||||
@@ -56,44 +68,47 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className = 'chatmessage-wrapper'>
|
||||
<div className = 'chatmessage'>
|
||||
{ this.props.showDisplayName && this._renderDisplayName() }
|
||||
<div className = 'usermessage'>
|
||||
{ processedMessage }
|
||||
</div>
|
||||
<div className = { `chatmessage ${messageTypeClassname}` }>
|
||||
<div className = 'chatArrow' />
|
||||
<div className = 'display-name'>
|
||||
{ message.displayName }
|
||||
</div>
|
||||
<div className = { 'timestamp' }>
|
||||
{ ChatMessage.formatTimestamp(message.timestamp) }
|
||||
</div>
|
||||
<div className = 'usermessage'>
|
||||
{ processedMessage }
|
||||
</div>
|
||||
{ this.props.showTimestamp && this._renderTimestamp() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_getFormattedTimestamp: () => string;
|
||||
|
||||
/**
|
||||
* Renders the display name of the sender.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderDisplayName() {
|
||||
return (
|
||||
<div className = 'display-name'>
|
||||
{ this.props.message.displayName }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the time at which the message was sent.
|
||||
* Returns a timestamp formatted for display.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
* @param {number} timestamp - The timestamp for the chat message.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_renderTimestamp() {
|
||||
return (
|
||||
<div className = 'timestamp'>
|
||||
{ this._getFormattedTimestamp() }
|
||||
</div>
|
||||
);
|
||||
static formatTimestamp(timestamp) {
|
||||
const now = new Date(timestamp);
|
||||
let hour = now.getHours();
|
||||
let minute = now.getMinutes();
|
||||
let second = now.getSeconds();
|
||||
|
||||
if (hour.toString().length === 1) {
|
||||
hour = `0${hour}`;
|
||||
}
|
||||
|
||||
if (minute.toString().length === 1) {
|
||||
minute = `0${minute}`;
|
||||
}
|
||||
|
||||
if (second.toString().length === 1) {
|
||||
second = `0${second}`;
|
||||
}
|
||||
|
||||
return `${hour}:${minute}:${second}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import ChatMessage from './ChatMessage';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Additional CSS classes to apply to the root element.
|
||||
*/
|
||||
className: string,
|
||||
|
||||
/**
|
||||
* The messages to display as a group.
|
||||
*/
|
||||
messages: Array<Object>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a list of chat messages. Will show only the display name for the
|
||||
* first chat message and the timestamp for the last chat message.
|
||||
*
|
||||
* @extends React.Component
|
||||
*/
|
||||
class ChatMessageGroup extends Component<Props> {
|
||||
static defaultProps = {
|
||||
className: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { className, messages } = this.props;
|
||||
|
||||
const messagesLength = messages.length;
|
||||
|
||||
if (!messagesLength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { `chat-message-group ${className}` }>
|
||||
{
|
||||
messages.map((message, i) => (
|
||||
<ChatMessage
|
||||
key = { i }
|
||||
message = { message }
|
||||
showDisplayName = { i === 0 }
|
||||
showTimestamp = { i === messages.length - 1 } />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatMessageGroup;
|
||||
@@ -1,123 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AbstractMessageContainer, { type Props }
|
||||
from '../AbstractMessageContainer';
|
||||
|
||||
import ChatMessageGroup from './ChatMessageGroup';
|
||||
|
||||
/**
|
||||
* Displays all received chat messages, grouped by sender.
|
||||
*
|
||||
* @extends AbstractMessageContainer
|
||||
*/
|
||||
export default class MessageContainer extends AbstractMessageContainer {
|
||||
/**
|
||||
* Whether or not chat has been scrolled to the bottom of the screen. Used
|
||||
* to determine if chat should be scrolled automatically to the bottom when
|
||||
* the {@code ChatInput} resizes.
|
||||
*/
|
||||
_isScrolledToBottom: boolean;
|
||||
|
||||
/**
|
||||
* Reference to the HTML element at the end of the list of displayed chat
|
||||
* messages. Used for scrolling to the end of the chat messages.
|
||||
*/
|
||||
_messagesListEndRef: Object;
|
||||
|
||||
/**
|
||||
* A React ref to the HTML element containing all {@code ChatMessageGroup}
|
||||
* instances.
|
||||
*/
|
||||
_messageListRef: Object;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code MessageContainer} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code MessageContainer} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._isScrolledToBottom = true;
|
||||
|
||||
this._messageListRef = React.createRef();
|
||||
this._messagesListEndRef = React.createRef();
|
||||
|
||||
this._onChatScroll = this._onChatScroll.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const groupedMessages = this._getMessagesGroupedBySender();
|
||||
const messages = groupedMessages.map((group, index) => {
|
||||
const messageType = group[0] && group[0].messageType;
|
||||
|
||||
return (
|
||||
<ChatMessageGroup
|
||||
className = { messageType || 'remote' }
|
||||
key = { index }
|
||||
messages = { group } />
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
id = 'chatconversation'
|
||||
onScroll = { this._onChatScroll }
|
||||
ref = { this._messageListRef }>
|
||||
{ messages }
|
||||
<div ref = { this._messagesListEndRef } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls to the bottom again if the instance had previously been scrolled
|
||||
* to the bottom. This method is used when a resize has occurred below the
|
||||
* instance and bottom scroll needs to be maintained.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
maybeUpdateBottomScroll() {
|
||||
if (this._isScrolledToBottom) {
|
||||
this.scrollToBottom(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically scrolls the displayed chat messages down to the latest.
|
||||
*
|
||||
* @param {boolean} withAnimation - Whether or not to show a scrolling
|
||||
* animation.
|
||||
* @returns {void}
|
||||
*/
|
||||
scrollToBottom(withAnimation: boolean) {
|
||||
this._messagesListEndRef.current.scrollIntoView({
|
||||
behavior: withAnimation ? 'smooth' : 'auto'
|
||||
});
|
||||
}
|
||||
|
||||
_getMessagesGroupedBySender: () => Array<Array<Object>>;
|
||||
|
||||
_onChatScroll: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked to listen to the current scroll location.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChatScroll() {
|
||||
const element = this._messageListRef.current;
|
||||
|
||||
this._isScrolledToBottom
|
||||
= element.scrollHeight - element.scrollTop === element.clientHeight;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import React from 'react';
|
||||
import { BackHandler, SafeAreaView, StatusBar, View } from 'react-native';
|
||||
|
||||
import { appNavigate } from '../../../app';
|
||||
import { connect, disconnect } from '../../../base/connection';
|
||||
import { getParticipantCount } from '../../../base/participants';
|
||||
import { Container, LoadingIndicator, TintedView } from '../../../base/react';
|
||||
import { connect as reactReduxConnect } from '../../../base/redux';
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
makeAspectRatioAware
|
||||
} from '../../../base/responsive-ui';
|
||||
import { TestConnectionInfo } from '../../../base/testing';
|
||||
import { createDesiredLocalTracks } from '../../../base/tracks';
|
||||
import { ConferenceNotification } from '../../../calendar-sync';
|
||||
import { Chat } from '../../../chat';
|
||||
import { DisplayNameLabel } from '../../../display-name';
|
||||
@@ -66,21 +64,6 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
_largeVideoParticipantId: string,
|
||||
|
||||
/**
|
||||
* Current conference's full URL.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_locationURL: URL,
|
||||
|
||||
/**
|
||||
* The handler which dispatches the (redux) action connect.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onConnect: Function,
|
||||
|
||||
/**
|
||||
* The handler which dispatches the (redux) action disconnect.
|
||||
*
|
||||
@@ -166,8 +149,6 @@ class Conference extends AbstractConference<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.props._onConnect();
|
||||
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.props._onHardwareBackPress);
|
||||
@@ -186,24 +167,14 @@ class Conference extends AbstractConference<Props, *> {
|
||||
*/
|
||||
componentDidUpdate(pevProps: Props) {
|
||||
const {
|
||||
_locationURL: oldLocationURL,
|
||||
_participantCount: oldParticipantCount,
|
||||
_room: oldRoom
|
||||
_participantCount: oldParticipantCount
|
||||
} = pevProps;
|
||||
const {
|
||||
_locationURL: newLocationURL,
|
||||
_participantCount: newParticipantCount,
|
||||
_room: newRoom,
|
||||
_setToolboxVisible,
|
||||
_toolboxVisible
|
||||
} = this.props;
|
||||
|
||||
// If the location URL changes we need to reconnect.
|
||||
oldLocationURL !== newLocationURL && newRoom && this.props._onDisconnect();
|
||||
|
||||
// Start the connection process when there is a (valid) room.
|
||||
oldRoom !== newRoom && newRoom && this.props._onConnect();
|
||||
|
||||
if (oldParticipantCount === 1
|
||||
&& newParticipantCount > 1
|
||||
&& _toolboxVisible) {
|
||||
@@ -228,8 +199,6 @@ class Conference extends AbstractConference<Props, *> {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.props._onHardwareBackPress);
|
||||
|
||||
this.props._onDisconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,36 +365,12 @@ class Conference extends AbstractConference<Props, *> {
|
||||
* @param {Function} dispatch - Redux action dispatcher.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _onConnect: Function,
|
||||
* _onDisconnect: Function,
|
||||
* _onHardwareBackPress: Function,
|
||||
* _setToolboxVisible: Function
|
||||
* }}
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
/**
|
||||
* Dispatches actions to create the desired local tracks and for
|
||||
* connecting to the conference.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onConnect() {
|
||||
dispatch(createDesiredLocalTracks());
|
||||
dispatch(connect());
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches an action disconnecting from the conference.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDisconnect() {
|
||||
dispatch(disconnect());
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles a hardware button press for back navigation. Leaves the
|
||||
* associated {@code Conference}.
|
||||
@@ -462,8 +407,7 @@ function _mapDispatchToProps(dispatch) {
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { connecting, connection, locationURL }
|
||||
= state['features/base/connection'];
|
||||
const { connecting, connection } = state['features/base/connection'];
|
||||
const {
|
||||
conference,
|
||||
joining,
|
||||
@@ -508,14 +452,6 @@ function _mapStateToProps(state) {
|
||||
*/
|
||||
_largeVideoParticipantId: state['features/large-video'].participantId,
|
||||
|
||||
/**
|
||||
* Current conference's full URL.
|
||||
*
|
||||
* @private
|
||||
* @type {URL}
|
||||
*/
|
||||
_locationURL: locationURL,
|
||||
|
||||
/**
|
||||
* The number of participants in the conference.
|
||||
*
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
|
||||
import { createDeviceChangedEvent, sendAnalytics } from '../analytics';
|
||||
import {
|
||||
getDeviceLabelById,
|
||||
setAudioInputDevice,
|
||||
setAudioOutputDeviceId,
|
||||
setVideoInputDevice
|
||||
@@ -113,9 +112,7 @@ export function submitDeviceSelectionTab(newState) {
|
||||
&& newState.selectedVideoInputId
|
||||
!== currentState.selectedVideoInputId) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: newState.selectedVideoInputId,
|
||||
userSelectedCameraDeviceLabel:
|
||||
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
|
||||
userSelectedCameraDeviceId: newState.selectedVideoInputId
|
||||
}));
|
||||
|
||||
dispatch(
|
||||
@@ -126,9 +123,7 @@ export function submitDeviceSelectionTab(newState) {
|
||||
&& newState.selectedAudioInputId
|
||||
!== currentState.selectedAudioInputId) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedMicDeviceId: newState.selectedAudioInputId,
|
||||
userSelectedMicDeviceLabel:
|
||||
getDeviceLabelById(getState(), newState.selectedAudioInputId, 'audioInput')
|
||||
userSelectedMicDeviceId: newState.selectedAudioInputId
|
||||
}));
|
||||
|
||||
dispatch(
|
||||
@@ -143,8 +138,7 @@ export function submitDeviceSelectionTab(newState) {
|
||||
setAudioOutputDeviceId(
|
||||
newState.selectedAudioOutputId,
|
||||
dispatch,
|
||||
true,
|
||||
getDeviceLabelById(getState(), newState.selectedAudioOutputId, 'audioOutput'))
|
||||
true)
|
||||
.then(() => logger.log('changed audio output device'))
|
||||
.catch(err => {
|
||||
logger.warn(
|
||||
|
||||
@@ -113,7 +113,6 @@ class AudioOutputPreview extends Component<Props> {
|
||||
*/
|
||||
_setAudioSink() {
|
||||
this._audioElement
|
||||
&& this.props.deviceId
|
||||
&& this._audioElement.setSinkId(this.props.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,6 @@ import {
|
||||
} from '../base/devices';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
import { toState } from '../base/redux';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
getUserSelectedMicDeviceId,
|
||||
getUserSelectedOutputDeviceId
|
||||
} from '../base/settings';
|
||||
|
||||
/**
|
||||
* Returns the properties for the device selection dialog from Redux state.
|
||||
@@ -43,9 +38,9 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
|
||||
// on welcome page we also show only what we have saved as user selected devices
|
||||
if (!conference) {
|
||||
disableAudioInputChange = false;
|
||||
selectedAudioInputId = getUserSelectedMicDeviceId(state);
|
||||
selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
|
||||
selectedVideoInputId = getUserSelectedCameraDeviceId(state);
|
||||
selectedAudioInputId = settings.userSelectedMicDeviceId;
|
||||
selectedAudioOutputId = settings.userSelectedAudioOutputDeviceId;
|
||||
selectedVideoInputId = settings.userSelectedCameraDeviceId;
|
||||
}
|
||||
|
||||
// we fill the device selection dialog with the devices that are currently
|
||||
|
||||
@@ -41,16 +41,6 @@ export const REMOVE_PENDING_INVITE_REQUESTS
|
||||
*/
|
||||
export const SET_CALLEE_INFO_VISIBLE = 'SET_CALLEE_INFO_VISIBLE';
|
||||
|
||||
/**
|
||||
* The type of Redux action to set the visibility of the dial in summary.
|
||||
*
|
||||
* {
|
||||
* type: SET_DIAL_IN_SUMMARY_VISIBLE,
|
||||
* visible: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_DIAL_IN_SUMMARY_VISIBLE = 'SET_DIAL_IN_SUMMARY_VISIBLE';
|
||||
|
||||
/**
|
||||
* The type of redux action which sets the invite dialog visible or invisible.
|
||||
*
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
BEGIN_ADD_PEOPLE,
|
||||
REMOVE_PENDING_INVITE_REQUESTS,
|
||||
SET_CALLEE_INFO_VISIBLE,
|
||||
SET_DIAL_IN_SUMMARY_VISIBLE,
|
||||
SET_INVITE_DIALOG_VISIBLE,
|
||||
UPDATE_DIAL_IN_NUMBERS_FAILED,
|
||||
UPDATE_DIAL_IN_NUMBERS_SUCCESS
|
||||
@@ -257,15 +256,6 @@ export function addPendingInviteRequest(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to hide the dial in summary.
|
||||
*
|
||||
* @returns {showDialInSummary}
|
||||
*/
|
||||
export function hideDialInSummary() {
|
||||
return showDialInSummary(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all pending invite requests.
|
||||
*
|
||||
@@ -278,19 +268,3 @@ export function removePendingInviteRequests() {
|
||||
type: REMOVE_PENDING_INVITE_REQUESTS
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to set the dial in summary url (and show it).
|
||||
*
|
||||
* @param {?string} locationUrl - The location URL to show the dial in summary for.
|
||||
* @returns {{
|
||||
* type: SET_DIAL_IN_SUMMARY_VISIBLE,
|
||||
* summaryUrl: ?string
|
||||
* }}
|
||||
*/
|
||||
export function showDialInSummary(locationUrl: ?string) {
|
||||
return {
|
||||
type: SET_DIAL_IN_SUMMARY_VISIBLE,
|
||||
summaryUrl: locationUrl
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ import { Icon } from '../../../../base/font-icons';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import {
|
||||
AvatarListItem,
|
||||
HeaderWithNavigation,
|
||||
BackButton,
|
||||
ForwardButton,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Modal,
|
||||
type Item
|
||||
} from '../../../../base/react';
|
||||
@@ -143,12 +146,14 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
<Modal
|
||||
onRequestClose = { this._onCloseAddPeopleDialog }
|
||||
visible = { this.props._isVisible }>
|
||||
<HeaderWithNavigation
|
||||
forwardDisabled = { this._isAddDisabled() }
|
||||
forwardLabelKey = 'inviteDialog.send'
|
||||
headerLabelKey = 'inviteDialog.header'
|
||||
onPressBack = { this._onCloseAddPeopleDialog }
|
||||
onPressForward = { this._onInvite } />
|
||||
<Header>
|
||||
<BackButton onPress = { this._onCloseAddPeopleDialog } />
|
||||
<HeaderLabel labelKey = 'inviteDialog.header' />
|
||||
<ForwardButton
|
||||
disabled = { this._isAddDisabled() }
|
||||
labelKey = 'inviteDialog.send'
|
||||
onPress = { this._onInvite } />
|
||||
</Header>
|
||||
<SafeAreaView style = { styles.dialogWrapper }>
|
||||
<View
|
||||
style = { styles.searchFieldWrapper }>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ConferenceID}.
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import ConferenceID from './ConferenceID';
|
||||
import NumbersList from './NumbersList';
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
// @flow
|
||||
|
||||
export { default as DialInSummary } from './DialInSummary';
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export * from './native';
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export * from './web';
|
||||
@@ -1,156 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Linking, View } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { type Dispatch } from 'redux';
|
||||
|
||||
import { openDialog } from '../../../../base/dialog';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import {
|
||||
HeaderWithNavigation,
|
||||
LoadingIndicator,
|
||||
SlidingView
|
||||
} from '../../../../base/react';
|
||||
import { connect } from '../../../../base/redux';
|
||||
|
||||
import { hideDialInSummary } from '../../../actions';
|
||||
import { getDialInfoPageURLForURIString } from '../../../functions';
|
||||
|
||||
import DialInSummaryErrorDialog from './DialInSummaryErrorDialog';
|
||||
import styles, { INDICATOR_COLOR } from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The URL to display the summary for.
|
||||
*/
|
||||
_summaryUrl: ?string,
|
||||
|
||||
dispatch: Dispatch<any>
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React native component that displays the dial in info page for a specific room.
|
||||
*/
|
||||
class DialInSummary extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Initializes a new instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onCloseView = this._onCloseView.bind(this);
|
||||
this._onError = this._onError.bind(this);
|
||||
this._onNavigate = this._onNavigate.bind(this);
|
||||
this._renderLoading = this._renderLoading.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _summaryUrl } = this.props;
|
||||
|
||||
return (
|
||||
<SlidingView
|
||||
position = 'bottom'
|
||||
show = { Boolean(_summaryUrl) } >
|
||||
<View style = { styles.webViewWrapper }>
|
||||
<HeaderWithNavigation
|
||||
headerLabelKey = 'info.label'
|
||||
onPressBack = { this._onCloseView } />
|
||||
<WebView
|
||||
onError = { this._onError }
|
||||
onShouldStartLoadWithRequest = { this._onNavigate }
|
||||
renderLoading = { this._renderLoading }
|
||||
source = {{ uri: getDialInfoPageURLForURIString(_summaryUrl) }}
|
||||
startInLoadingState = { true }
|
||||
style = { styles.webView } />
|
||||
</View>
|
||||
</SlidingView>
|
||||
);
|
||||
}
|
||||
|
||||
_onCloseView: () => void;
|
||||
|
||||
/**
|
||||
* Closes the view.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCloseView() {
|
||||
this.props.dispatch(hideDialInSummary());
|
||||
}
|
||||
|
||||
_onError: () => void;
|
||||
|
||||
/**
|
||||
* Callback to handle the error if the page fails to load.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onError() {
|
||||
this.props.dispatch(hideDialInSummary());
|
||||
this.props.dispatch(openDialog(DialInSummaryErrorDialog));
|
||||
}
|
||||
|
||||
_onNavigate: Object => Boolean;
|
||||
|
||||
/**
|
||||
* Callback to intercept navigation inside the webview and make the native app handle the dial requests.
|
||||
*
|
||||
* NOTE: We don't navigate to anywhere else form that view.
|
||||
*
|
||||
* @param {any} request - The request object.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onNavigate(request) {
|
||||
const { url } = request;
|
||||
|
||||
if (url.startsWith('tel:')) {
|
||||
Linking.openURL(url);
|
||||
this.props.dispatch(hideDialInSummary());
|
||||
}
|
||||
|
||||
return url === getDialInfoPageURLForURIString(this.props._summaryUrl);
|
||||
}
|
||||
|
||||
_renderLoading: () => React$Component<any>;
|
||||
|
||||
/**
|
||||
* Renders the loading indicator.
|
||||
*
|
||||
* @returns {React$Component<any>}
|
||||
*/
|
||||
_renderLoading() {
|
||||
return (
|
||||
<View style = { styles.indicatorWrapper }>
|
||||
<LoadingIndicator
|
||||
color = { INDICATOR_COLOR }
|
||||
size = 'large' />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {{
|
||||
* _summaryUrl: ?string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_summaryUrl: state['features/invite'].summaryUrl
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(DialInSummary));
|
||||
@@ -1,29 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { AlertDialog } from '../../../../base/dialog';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { connect } from '../../../../base/redux';
|
||||
|
||||
/**
|
||||
* Dialog to inform the user that we could't fetch the dial-in info page.
|
||||
*/
|
||||
class DialInSummaryErrorDialog extends Component<{}> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<AlertDialog
|
||||
contentKey = 'info.dialInSummaryError' />
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
}
|
||||
|
||||
export default translate(connect()(DialInSummaryErrorDialog));
|
||||
@@ -1,24 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { ColorPalette } from '../../../../base/styles';
|
||||
|
||||
export const INDICATOR_COLOR = ColorPalette.lightGrey;
|
||||
|
||||
export default {
|
||||
|
||||
indicatorWrapper: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: ColorPalette.white,
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
webView: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
webViewWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'column'
|
||||
}
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export { default as DialInSummary } from './DialInSummary';
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
export * from './add-people-dialog';
|
||||
export * from './dial-in-summary';
|
||||
export { DialInSummary } from './dial-in-summary';
|
||||
export * from './info-dialog';
|
||||
export * from './callee-info';
|
||||
|
||||
@@ -8,10 +8,7 @@ import { getInviteURL } from '../../../../base/connection';
|
||||
import { Dialog } from '../../../../base/dialog';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import {
|
||||
isLocalParticipantModerator,
|
||||
getLocalParticipant
|
||||
} from '../../../../base/participants';
|
||||
import { isLocalParticipantModerator } from '../../../../base/participants';
|
||||
|
||||
import { _getDefaultPhoneNumber, getDialInfoPageURL } from '../../../functions';
|
||||
import DialInNumber from './DialInNumber';
|
||||
@@ -45,11 +42,6 @@ type Props = {
|
||||
*/
|
||||
_inviteURL: string,
|
||||
|
||||
/**
|
||||
* The redux representation of the local participant.
|
||||
*/
|
||||
_localParticipant: Object,
|
||||
|
||||
/**
|
||||
* The current location url of the conference.
|
||||
*/
|
||||
@@ -301,18 +293,14 @@ class InfoDialog extends Component<Props, State> {
|
||||
* @returns {string}
|
||||
*/
|
||||
_getTextToCopy() {
|
||||
const { _localParticipant, liveStreamViewURL, t } = this.props;
|
||||
const { liveStreamViewURL, t } = this.props;
|
||||
const shouldDisplayDialIn = this._shouldDisplayDialIn();
|
||||
const moreInfo
|
||||
= shouldDisplayDialIn
|
||||
? t('info.inviteURLMoreInfo', { conferenceID: this.props.dialIn.conferenceID })
|
||||
: '';
|
||||
|
||||
let invite = _localParticipant && _localParticipant.name
|
||||
? t('info.inviteURLFirstPartPersonal', { name: _localParticipant.name })
|
||||
: t('info.inviteURLFirstPartGeneral');
|
||||
|
||||
invite += t('info.inviteURLSecondPart', {
|
||||
let invite = t('info.inviteURL', {
|
||||
url: this.props._inviteURL,
|
||||
moreInfo
|
||||
});
|
||||
@@ -588,11 +576,10 @@ function _mapStateToProps(state) {
|
||||
} = state['features/base/conference'];
|
||||
|
||||
return {
|
||||
_canEditPassword: isLocalParticipantModerator(state, state['features/base/config'].lockRoomGuestEnabled),
|
||||
_canEditPassword: isLocalParticipantModerator(state),
|
||||
_conference: conference,
|
||||
_conferenceName: room,
|
||||
_inviteURL: getInviteURL(state),
|
||||
_localParticipant: getLocalParticipant(state),
|
||||
_locationURL: state['features/base/connection'].locationURL,
|
||||
_locked: locked,
|
||||
_password: password
|
||||
|
||||
@@ -508,22 +508,6 @@ export function getDialInfoPageURL(
|
||||
return `${origin}${newPath}/static/dialInInfo.html?room=${conferenceName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the URL for the static dial in info page.
|
||||
*
|
||||
* @param {string} uri - The conference URI string.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDialInfoPageURLForURIString(
|
||||
uri: ?string) {
|
||||
if (!uri) {
|
||||
return undefined;
|
||||
}
|
||||
const { protocol, host, contextRoot, room } = parseURIString(uri);
|
||||
|
||||
return `${protocol}//${host}${contextRoot}static/dialInInfo.html?room=${room}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal state of which dial-in number to display.
|
||||
*
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
ADD_PENDING_INVITE_REQUEST,
|
||||
REMOVE_PENDING_INVITE_REQUESTS,
|
||||
SET_CALLEE_INFO_VISIBLE,
|
||||
SET_DIAL_IN_SUMMARY_VISIBLE,
|
||||
SET_INVITE_DIALOG_VISIBLE,
|
||||
UPDATE_DIAL_IN_NUMBERS_FAILED,
|
||||
UPDATE_DIAL_IN_NUMBERS_SUCCESS
|
||||
@@ -51,12 +50,6 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
|
||||
initialCalleeInfo: action.initialCalleeInfo
|
||||
};
|
||||
|
||||
case SET_DIAL_IN_SUMMARY_VISIBLE:
|
||||
return {
|
||||
...state,
|
||||
summaryUrl: action.summaryUrl
|
||||
};
|
||||
|
||||
case SET_INVITE_DIALOG_VISIBLE:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -7,8 +7,6 @@ import { getDefaultURL } from '../../app';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { NavigateSectionList, type Section } from '../../base/react';
|
||||
import { connect } from '../../base/redux';
|
||||
import { ColorPalette } from '../../base/styles';
|
||||
import { showDialInSummary } from '../../invite';
|
||||
|
||||
import { deleteRecentListEntry } from '../actions';
|
||||
import { isRecentListEnabled, toDisplayableList } from '../functions';
|
||||
@@ -62,7 +60,6 @@ class RecentList extends AbstractRecentList<Props> {
|
||||
super(props);
|
||||
|
||||
this._onDelete = this._onDelete.bind(this);
|
||||
this._onShowDialInInfo = this._onShowDialInInfo.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,10 +79,6 @@ class RecentList extends AbstractRecentList<Props> {
|
||||
} = this.props;
|
||||
const recentList = toDisplayableList(_recentList, t, _defaultServerURL);
|
||||
const slideActions = [ {
|
||||
backgroundColor: ColorPalette.blue,
|
||||
onPress: this._onShowDialInInfo,
|
||||
text: t('welcomepage.info')
|
||||
}, {
|
||||
backgroundColor: 'red',
|
||||
onPress: this._onDelete,
|
||||
text: t('welcomepage.recentListDelete')
|
||||
@@ -114,18 +107,6 @@ class RecentList extends AbstractRecentList<Props> {
|
||||
_onDelete(itemId) {
|
||||
this.props.dispatch(deleteRecentListEntry(itemId));
|
||||
}
|
||||
|
||||
_onShowDialInInfo: Object => void
|
||||
|
||||
/**
|
||||
* Callback for the dial-in info action of the list.
|
||||
*
|
||||
* @param {Object} itemId - The ID of the entry for which we'd like to show the dial in numbers.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShowDialInInfo(itemId) {
|
||||
this.props.dispatch(showDialInSummary(itemId.url));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,10 +66,7 @@ class StreamKeyForm extends AbstractStreamKeyForm<Props> {
|
||||
onChangeText = { this._onInputChange }
|
||||
placeholder = { t('liveStreaming.enterStreamKey') }
|
||||
placeholderTextColor = { PLACEHOLDER_COLOR }
|
||||
style = { [
|
||||
_dialogStyles.text,
|
||||
styles.streamKeyInput
|
||||
] }
|
||||
style = { styles.streamKeyInput }
|
||||
value = { this.props.value } />
|
||||
<View style = { styles.formFooter }>
|
||||
{
|
||||
|
||||
@@ -90,10 +90,9 @@ export default createStyleSheet({
|
||||
alignSelf: 'stretch',
|
||||
borderColor: ColorPalette.lightGrey,
|
||||
borderBottomWidth: 1,
|
||||
fontSize: 14,
|
||||
color: ColorPalette.white,
|
||||
height: 40,
|
||||
marginBottom: 5,
|
||||
textAlign: 'left'
|
||||
marginBottom: 5
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -106,12 +106,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
|
||||
case RECORDING_SESSION_UPDATED: {
|
||||
// When in recorder mode no notifications are shown
|
||||
// or extra sounds are also not desired
|
||||
if (getState()['features/base/config'].iAmRecorder) {
|
||||
break;
|
||||
}
|
||||
|
||||
const updatedSessionData
|
||||
= getSessionById(getState(), action.sessionData.id);
|
||||
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Alert, NativeModules, SafeAreaView, ScrollView, Switch, Text, TextInput
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { HeaderWithNavigation, Modal } from '../../../base/react';
|
||||
import { BackButton, Header, Modal } from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import {
|
||||
@@ -18,6 +18,7 @@ import FormRow from './FormRow';
|
||||
import FormSectionHeader from './FormSectionHeader';
|
||||
import { normalizeUserInputURL } from '../../functions';
|
||||
import styles from './styles';
|
||||
import { HeaderLabel } from '../../../base/react/components/native';
|
||||
|
||||
/**
|
||||
* Application information module.
|
||||
@@ -212,9 +213,10 @@ class SettingsView extends AbstractSettingsView<Props> {
|
||||
*/
|
||||
_renderHeader() {
|
||||
return (
|
||||
<HeaderWithNavigation
|
||||
headerLabelKey = 'settingsView.header'
|
||||
onPressBack = { this._onRequestClose } />
|
||||
<Header>
|
||||
<BackButton onPress = { this._onRequestClose } />
|
||||
<HeaderLabel labelKey = 'settingsView.header' />
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
createDesiredLocalTracks,
|
||||
destroyLocalTracks
|
||||
} from '../../base/tracks';
|
||||
import { DialInSummary } from '../../invite';
|
||||
import { SettingsView } from '../../settings';
|
||||
|
||||
import {
|
||||
@@ -136,7 +135,6 @@ class WelcomePage extends AbstractWelcomePage {
|
||||
</SafeAreaView>
|
||||
<WelcomePageLists disabled = { this.state._fieldFocused } />
|
||||
<SettingsView />
|
||||
<DialInSummary />
|
||||
</View>
|
||||
<WelcomePageSideBar />
|
||||
</LocalVideoTrackUnderlay>
|
||||
|
||||
@@ -119,9 +119,7 @@ class WelcomePage extends AbstractWelcomePage {
|
||||
className = { `welcome ${showAdditionalContent
|
||||
? 'with-content' : 'without-content'}` }
|
||||
id = 'welcome_page'>
|
||||
<div className = 'welcome-watermark'>
|
||||
<Watermarks />
|
||||
</div>
|
||||
<Watermarks />
|
||||
<div className = 'header'>
|
||||
<div className = 'welcome-page-settings'>
|
||||
<SettingsButton
|
||||
|
||||
Reference in New Issue
Block a user