Compare commits

...

14 Commits

Author SHA1 Message Date
Calinteodor
87a110b9c3 fix: improved copy text helper function (#8677) 2021-02-24 09:12:41 -06:00
Mihai-Andrei Uscat
a7db7ecaff fix(LargeVideo): Fix large video not resizing when closing chat. 2021-02-24 14:26:00 +02:00
horymury
79bb98dab3 (feature) - Add support for custom DID numbers page url 2021-02-24 11:37:14 +02:00
Gabriel Imre
d22792c9e3 feat(sip): Added auto-knocking for sip gateway if lobby is enabled
Co-authored-by: Gabriel Imre <gabriel.lucaci@8x8.com>
2021-02-24 11:35:32 +02:00
Avram Tudor
41e6af3464 Merge pull request #8120 from jitsi/tavram/slowgum
fix(gum) add event handling for SLOW_GET_USER_MEDIA
2021-02-23 14:45:36 +02:00
Tudor-Ovidiu Avram
f50fd7b7bd fix(gum) add event handling for SLOW_GET_USER_MEDIA
Show an overlay with a spinner when slow gUM is fired
2021-02-23 13:51:24 +02:00
Mihai-Andrei Uscat
43761fc398 feat(Chat) Improve responsive behaviour further.
* Add buttons to send messages/set nickname.
* Redesign message/nickname inputs.
* Pin messages to the input.
* Add keyboard avoider for Safari.
* Make chat content scrollable on mobile.
2021-02-23 09:39:20 +02:00
damencho
4c39d83ff1 feat(load-test): Fixes audio senders. 2021-02-22 17:48:19 -06:00
Jaya Allamsetty
e525c2b2ec chore(deps) lib-jitsi-meet@latest
* fix(SendVideoController): Apply the sender constraint only when it changes. There were cases where the bridge was sending the same constraint multiple times causing redundant calls to getParameters/setParameters on the RTCRtpSender.
* fix(gum) update permissions prompt detection

beaff3dd02...7f919faacc
2021-02-19 12:04:25 -05:00
Tudor D. Pop
f69a31d9c6 fix(blur) check model response status and catch errors 2021-02-19 15:00:07 +01:00
tmoldovan8x8
67930edae2 chore(ios) remove warnings on JitsiMeetView 2021-02-19 10:40:45 +02:00
Jaya Allamsetty
c11a94f7d7 feat: Add 'useNewBandwidthAllocationStrategy' to config.js. 2021-02-18 14:30:39 -05:00
Jaya Allamsetty
bfd093b0ba chore(deps) lib-jitsi-meet@latest
* feat: Use the new bridge signaling format.

c534f74884...beaff3dd02
2021-02-18 14:30:39 -05:00
Saúl Ibarra Corretgé
861935c9d7 fix(blur) fix model paths 2021-02-18 15:57:01 +01:00
44 changed files with 729 additions and 297 deletions

View File

@@ -115,7 +115,7 @@ import {
submitFeedback
} from './react/features/feedback';
import { showNotification } from './react/features/notifications';
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
import { mediaPermissionPromptVisibilityChanged, toggleSlowGUMOverlay } from './react/features/overlay';
import { suspendDetected } from './react/features/power-monitor';
import {
initPrejoin,
@@ -502,6 +502,11 @@ export default {
);
}
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMediaDevicesEvents.SLOW_GET_USER_MEDIA,
() => APP.store.dispatch(toggleSlowGUMOverlay(true))
);
let tryCreateLocalTracks;
// On Electron there is no permission prompt for granting permissions. That's why we don't need to
@@ -519,8 +524,10 @@ export default {
return createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true)
timeout,
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
.then(([ audioStream ]) =>
[ desktopStream, audioStream ])
.catch(error => {
@@ -536,8 +543,10 @@ export default {
return requestedAudio
? createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true)
timeout,
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
: [];
})
.catch(error => {
@@ -551,8 +560,10 @@ export default {
} else {
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
timeout
}, true)
timeout,
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
.catch(err => {
if (requestedAudio && requestedVideo) {
@@ -574,8 +585,10 @@ export default {
return (
createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true));
timeout,
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
}));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
@@ -598,8 +611,9 @@ export default {
return requestedVideo
? createLocalTracksF({
devices: [ 'video' ],
timeout
}, true)
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
: [];
})
.catch(err => {
@@ -619,6 +633,7 @@ export default {
// the user inputs their credentials, but the dialog would be
// overshadowed by the overlay.
tryCreateLocalTracks.then(tracks => {
APP.store.dispatch(toggleSlowGUMOverlay(false));
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
return tracks;
@@ -882,7 +897,7 @@ export default {
showUI && APP.store.dispatch(notifyMicError(error));
};
createLocalTracksF({ devices: [ 'audio' ] }, false)
createLocalTracksF({ devices: [ 'audio' ] })
.then(([ audioTrack ]) => audioTrack)
.catch(error => {
maybeShowErrorDialog(error);
@@ -996,7 +1011,7 @@ export default {
//
// FIXME when local track creation is moved to react/redux
// it should take care of the use case described above
createLocalTracksF({ devices: [ 'video' ] }, false)
createLocalTracksF({ devices: [ 'video' ] })
.then(([ videoTrack ]) => videoTrack)
.catch(error => {
// FIXME should send some feedback to the API on error ?

View File

@@ -240,6 +240,12 @@ var config = {
// 90: 2
// },
// Provides a way to translate the legacy bridge signaling messages, 'LastNChangedEvent',
// 'SelectedEndpointsChangedEvent' and 'ReceiverVideoConstraint' into the new 'ReceiverVideoConstraints' message
// that invokes the new bandwidth allocation algorithm in the bridge which is described here
// - https://github.com/jitsi/jitsi-videobridge/blob/master/doc/allocation.md.
// useNewBandwidthAllocationStrategy: false,
// Specify the settings for video quality optimizations on the client.
// videoQuality: {
// // Provides a way to prevent a video codec from being negotiated on the JVB connection. The codec specified

View File

@@ -1,3 +1,21 @@
/**
* Mixins that mimic the way Atlaskit fills the screen with modals at low screen widths.
*/
@mixin full-size-modal-positioner() {
height: 100%;
left: 0;
position: fixed;
top: 0;
max-width: 100%;
width: 100%;
}
@mixin full-size-modal-dialog() {
height: 100%;
max-height: 100%;
border-radius: 0;
}
/**
* Move the @atlaskit/flag container up a little bit so it does not cover the
* toolbar with the first notification.
@@ -56,4 +74,43 @@
.toolbox-button-wth-dialog > div:nth-child(2) {
max-height: calc(100vh - #{$newToolbarSizeWithPadding} - 46px);
overflow-y: auto;
}
}
/**
* The following selectors keep the chat modal full-size anywhere between 100px
* and 580px for desktop or 680px for mobile.
*/
@media (min-width: 100px) and (max-width: 320px) {
.smiley-input {
display: none;
}
.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
@media (min-width: 480px) and (max-width: 580px) {
.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
@media (min-width: 580px) and (max-width: 680px) {
.mobile-browser {
&.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
&.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
}

View File

@@ -32,6 +32,13 @@
width: $sidebarWidth;
word-wrap: break-word;
display: flex;
flex-direction: column;
& > :first-child {
margin-top: auto;
}
a {
display: block;
}
@@ -122,16 +129,61 @@
}
}
.chat-input-container {
padding: 0 16px 24px;
&.populated {
#chat-input {
border: 1px solid #619CF4;
.send-button {
background: #1B67EC;
cursor: pointer;
path {
fill: #fff;
}
}
}
}
}
#chat-input {
border-top: 1px solid $chatInputSeparatorColor;
border: 1px solid $chatInputSeparatorColor;
display: flex;
padding: 5px 10px;
border-radius: 3px;
* {
background-color: transparent;
}
}
.send-button-container {
display: flex;
align-items: center;
}
.send-button {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
width: 40px;
border-radius: 3px;
path {
fill: $chatInputSeparatorColor;
}
}
.mobile-browser {
.send-button {
height: 48px;
width: 48px;
}
}
.remoteuser {
color: #B8C7E0;
}
@@ -161,10 +213,47 @@
#nickname {
text-align: center;
color: #9d9d9d;
font-size: 18px;
margin-top: 30px;
left: 5px;
right: 5px;
font-size: 16px;
margin: auto 0;
padding: 0 16px;
input {
height: 40px;
}
label {
line-height: 24px;
}
.enter-chat {
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
height: 40px;
background: #1B67EC;
border-radius: 3px;
color: #fff;
cursor: pointer;
&.disabled {
color: #757575;
background: #11336E;
pointer-events: none;
}
}
}
.mobile-browser {
#nickname {
input {
height: 48px;
}
.enter-chat {
height: 48px;
}
}
}
.sideToolbarContainer {
@@ -411,6 +500,16 @@
#chatconversation {
width: 100%;
}
.chat-input-container {
padding: 0 0 24px;
}
}
.touchmove-hack {
display: flex;
flex: 1;
overflow: auto;
}
/**

View File

@@ -53,21 +53,6 @@
}
}
@mixin full-size-modal-positioner() {
height: 100%;
left: 0;
position: fixed;
top: 0;
max-width: 100%;
width: 100%;
}
@mixin full-size-modal-dialog() {
height: 100%;
max-height: 100%;
border-radius: 0;
}
@media only screen and (max-width: $verySmallScreen) {
.welcome {
display: block;
@@ -165,25 +150,3 @@
}
}
}
@media (min-width: 480px) and (max-width: 580px) {
.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
@media (min-width: 580px) and (max-width: 680px) {
.mobile-browser {
&.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
&.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
}

View File

@@ -33,4 +33,12 @@
bottom: 24px;
width: 100%;
}
&__spinner-container {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
}
}

View File

@@ -20,11 +20,11 @@
- (void)sendHangUp;
- (void)sendSetAudioMuted:(BOOL)muted;
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
- (void)sendEndpointTextMessage:(NSString*)message :(NSString*)to;
- (void)toggleScreenShare;
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completion;
- (void)openChat:(NSString*)to;
- (void)closeChat;
- (void)sendChatMessage:(NSString*)to :(NSString*)message;
- (void)sendChatMessage:(NSString*)message :(NSString*)to ;
@end

View File

@@ -153,7 +153,7 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[self sendEventWithName:setAudioMutedAction body:data];
}
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
- (void)sendEndpointTextMessage:(NSString*)message :(NSString*)to {
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
data[@"to"] = to;
data[@"message"] = message;
@@ -185,7 +185,7 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[self sendEventWithName:closeChatAction body:nil];
}
- (void)sendChatMessage:(NSString*)to :(NSString*)message {
- (void)sendChatMessage:(NSString*)message :(NSString*)to {
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
data[@"to"] = to;
data[@"message"] = message;

View File

@@ -38,11 +38,11 @@
- (void)leave;
- (void)hangUp;
- (void)setAudioMuted:(BOOL)muted;
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)toggleScreenShare;
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler;
- (void)openChat:(NSString*)to;
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler;
- (void)openChat:(NSString * _Nullable)to;
- (void)closeChat;
- (void)sendChatMessage:(NSString*)to :(NSString*)message;
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
@end

View File

@@ -125,9 +125,9 @@ static void initializeViewsMap() {
[externalAPI sendSetAudioMuted:muted];
}
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendEndpointTextMessage:to :message];
[externalAPI sendEndpointTextMessage:message :to];
}
- (void)toggleScreenShare {
@@ -135,7 +135,7 @@ static void initializeViewsMap() {
[externalAPI toggleScreenShare];
}
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler {
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI retrieveParticipantsInfo:completionHandler];
}
@@ -150,9 +150,9 @@ static void initializeViewsMap() {
[externalAPI closeChat];
}
- (void)sendChatMessage:(NSString*)to :(NSString*)message {
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendChatMessage:to :message];
[externalAPI sendChatMessage:message :to];
}
#pragma mark Private methods

View File

@@ -61,6 +61,7 @@
"today": "Today"
},
"chat": {
"enter": "Enter chat room",
"error": "Error: your message was not sent. Reason: {{error}}",
"fieldPlaceHolder": "Type your message here",
"messagebox": "Type a message",

View File

@@ -2,9 +2,9 @@
import React, { useState } from 'react';
import { translate } from '../../base/i18n';
import { Icon, IconCheck, IconCopy } from '../../base/icons';
import { copyText } from '../../base/util';
import { translate } from '../i18n';
import { copyText } from '../util';
type Props = {

View File

@@ -53,6 +53,11 @@ type Props = {
*/
hideCancelButton: boolean,
/**
* If true, no footer will be displayed.
*/
disableFooter?: boolean,
i18n: Object,
/**
@@ -174,6 +179,10 @@ class StatelessDialog extends Component<Props> {
this._renderCancelButton()
].filter(Boolean);
if (this.props.disableFooter) {
return null;
}
return (
<ModalFooter showKeyline = { propsFromModalFooter.showKeyline } >
{

View File

@@ -11,6 +11,16 @@ export function isMobileBrowser() {
return Platform.OS === 'android' || Platform.OS === 'ios';
}
/**
* Returns whether or not the current environment is an ios mobile device.
*
* @returns {boolean}
*/
export function isIosMobileBrowser() {
return Platform.OS === 'ios';
}
/**
* Checks whether the chrome extensions defined in the config file are installed or not.
*

View File

@@ -73,6 +73,7 @@ export { default as IconOpenInNew } from './open_in_new.svg';
export { default as IconOutlook } from './office365.svg';
export { default as IconPhone } from './phone.svg';
export { default as IconPin } from './enlarge.svg';
export { default as IconPlane } from './paper-plane.svg';
export { default as IconPresentation } from './presentation.svg';
export { default as IconRaisedHand } from './raised-hand.svg';
export { default as IconRec } from './rec.svg';

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6667 1.66663L1.66669 10.8333L7.6326 11.8276L8.33335 17.0833L10.644 13.2323L16.6667 17.9166V1.66663ZM8.73722 10.3221L6.35041 9.92426L15 4.63839V14.5089L11.3161 11.6436L12.5 7.49996L8.73722 10.3221Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -127,7 +127,6 @@ export function createLocalTracksA(options = {}) {
options.facingMode || CAMERA_FACING_MODE.USER,
micDeviceId: options.micDeviceId
},
/* firePermissionPromptIsShownEvent */ false,
store)
.then(
localTracks => {

View File

@@ -63,16 +63,25 @@ export async function createLocalPresenterTrack(options, desktopHeight) {
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {boolean} [firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* should check for a {@code getUserMedia} permission prompt and fire a
* corresponding event.
* @param {boolean} [options.fireSlowPromiseEvent] - Whether lib-jitsi-meet
* should check for a slow {@code getUserMedia} request and fire a
* corresponding event.
* @param {Object} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @returns {Promise<JitsiLocalTrack[]>}
*/
export function createLocalTracksF(options = {}, firePermissionPromptIsShownEvent, store) {
export function createLocalTracksF(options = {}, store) {
let { cameraDeviceId, micDeviceId } = options;
const { desktopSharingSourceDevice, desktopSharingSources, timeout } = options;
const {
desktopSharingSourceDevice,
desktopSharingSources,
firePermissionPromptIsShownEvent,
fireSlowPromiseEvent,
timeout
} = options;
if (typeof APP !== 'undefined') {
// TODO The app's settings should go in the redux store and then the
@@ -114,11 +123,12 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
devices: options.devices.slice(0),
effects,
firefox_fake_device, // eslint-disable-line camelcase
firePermissionPromptIsShownEvent,
fireSlowPromiseEvent,
micDeviceId,
resolution,
timeout
},
firePermissionPromptIsShownEvent)
})
.catch(err => {
logger.error('Failed to create local tracks', options.devices, err);
@@ -161,7 +171,10 @@ export function createPrejoinTracks() {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
firePermissionPromptIsShownEvent: true
})
.catch(err => {
if (requestedAudio && requestedVideo) {
@@ -169,7 +182,10 @@ export function createPrejoinTracks() {
errors.audioAndVideoError = err;
return (
createLocalTracksF({ devices: [ 'audio' ] }, true));
createLocalTracksF({
devices: [ 'audio' ],
firePermissionPromptIsShownEvent: true
}));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
@@ -190,7 +206,10 @@ export function createPrejoinTracks() {
// Try video only...
return requestedVideo
? createLocalTracksF({ devices: [ 'video' ] }, true)
? createLocalTracksF({
devices: [ 'video' ],
firePermissionPromptIsShownEvent: true
})
: [];
})
.catch(err => {

View File

@@ -31,24 +31,15 @@ export function assignIfDefined(target: Object, source: Object) {
* @param {string} textToCopy - Text to be copied.
* @returns {boolean}
*/
export function copyText(textToCopy: string) {
const fakeTextArea = document.createElement('textarea');
export async function copyText(textToCopy: string) {
let result;
// $FlowFixMe
document.body.appendChild(fakeTextArea);
fakeTextArea.value = textToCopy;
fakeTextArea.select();
try {
result = document.execCommand('copy');
result = await navigator.clipboard.writeText(textToCopy);
} catch (err) {
result = false;
}
// $FlowFixMe
document.body.removeChild(fakeTextArea);
return result;
}

View File

@@ -20,9 +20,10 @@ export * from './actions.any';
*/
export function openChat(participant: Object) {
return function(dispatch: (Object) => Object) {
dispatch({ participant,
type: OPEN_CHAT });
VideoLayout.onResize();
dispatch({
participant,
type: OPEN_CHAT
});
};
}
@@ -40,5 +41,8 @@ export function toggleChat() {
} else {
dispatch(openChat());
}
// Recompute the large video size whenever we toggle the chat, as it takes chat state into account.
VideoLayout.onResize();
};
}

View File

@@ -4,7 +4,6 @@ import React from 'react';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { toggleChat } from '../../actions.web';
import AbstractChat, {
_mapStateToProps,
type Props
@@ -14,8 +13,10 @@ import ChatDialog from './ChatDialog';
import Header from './ChatDialogHeader';
import ChatInput from './ChatInput';
import DisplayNameForm from './DisplayNameForm';
import KeyboardAvoider from './KeyboardAvoider';
import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
import TouchmoveHack from './TouchmoveHack';
/**
* React Component for holding the chat feature in a side panel that slides in
@@ -50,7 +51,6 @@ class Chat extends AbstractChat<Props> {
// Bind event handlers so they are only bound once for every instance.
this._renderPanelContent = this._renderPanelContent.bind(this);
this._onChatInputResize = this._onChatInputResize.bind(this);
this._onToggleChat = this._onToggleChat.bind(this);
}
/**
@@ -112,13 +112,16 @@ class Chat extends AbstractChat<Props> {
_renderChat() {
return (
<>
<MessageContainer
messages = { this.props._messages }
ref = { this._messageContainerRef } />
<TouchmoveHack isModal = { this.props._isModal }>
<MessageContainer
messages = { this.props._messages }
ref = { this._messageContainerRef } />
</TouchmoveHack>
<MessageRecipient />
<ChatInput
onResize = { this._onChatInputResize }
onSend = { this._onSendMessage } />
<KeyboardAvoider />
</>
);
}
@@ -132,9 +135,7 @@ class Chat extends AbstractChat<Props> {
*/
_renderChatHeader() {
return (
<Header
className = 'chat-header'
onCancel = { this._onToggleChat } />
<Header className = 'chat-header' />
);
}
@@ -198,18 +199,6 @@ class Chat extends AbstractChat<Props> {
}
_onSendMessage: (string) => void;
_onToggleChat: () => void;
/**
* Toggles the chat window.
*
* @returns {Function}
*/
_onToggleChat() {
this.props.dispatch(toggleChat());
}
}
export default translate(connect(_mapStateToProps)(Chat));

View File

@@ -24,6 +24,7 @@ function ChatDialog({ children }: Props) {
<Dialog
customHeader = { Header }
disableEnter = { true }
disableFooter = { true }
hideCancelButton = { true }
submitDisabled = { true }
titleKey = 'chat.title'>

View File

@@ -5,7 +5,7 @@ import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconClose } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { closeChat } from '../../actions.any';
import { toggleChat } from '../../actions.web';
type Props = {
@@ -42,6 +42,6 @@ function Header({ onCancel, className, t }: Props) {
);
}
const mapDispatchToProps = { onCancel: closeChat };
const mapDispatchToProps = { onCancel: toggleChat };
export default translate(connect(null, mapDispatchToProps)(Header));

View File

@@ -6,6 +6,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import type { Dispatch } from 'redux';
import { translate } from '../../../base/i18n';
import { Icon, IconPlane } from '../../../base/icons';
import { connect } from '../../../base/redux';
import SmileysPanel from './SmileysPanel';
@@ -81,6 +82,7 @@ class ChatInput extends Component<Props, State> {
this._onDetectSubmit = this._onDetectSubmit.bind(this);
this._onMessageChange = this._onMessageChange.bind(this);
this._onSmileySelect = this._onSmileySelect.bind(this);
this._onSubmitMessage = this._onSubmitMessage.bind(this);
this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
this._setTextAreaRef = this._setTextAreaRef.bind(this);
}
@@ -109,30 +111,39 @@ class ChatInput extends Component<Props, State> {
? 'show-smileys' : 'hide-smileys'} smileys-panel`;
return (
<div id = 'chat-input' >
<div className = 'smiley-input'>
<div id = 'smileysarea'>
<div id = 'smileys'>
<Emoji
onClick = { this._onToggleSmileysPanel }
text = ':)' />
<div className = { `chat-input-container${this.state.message.trim().length ? ' populated' : ''}` }>
<div id = 'chat-input' >
<div className = 'smiley-input'>
<div id = 'smileysarea'>
<div id = 'smileys'>
<Emoji
onClick = { this._onToggleSmileysPanel }
text = ':)' />
</div>
</div>
<div className = { smileysPanelClassName }>
<SmileysPanel
onSmileySelect = { this._onSmileySelect } />
</div>
</div>
<div className = { smileysPanelClassName }>
<SmileysPanel
onSmileySelect = { this._onSmileySelect } />
<div className = 'usrmsg-form'>
<TextareaAutosize
id = 'usermsg'
inputRef = { this._setTextAreaRef }
maxRows = { 5 }
onChange = { this._onMessageChange }
onHeightChange = { this.props.onResize }
onKeyDown = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') }
value = { this.state.message } />
</div>
<div className = 'send-button-container'>
<div
className = 'send-button'
onClick = { this._onSubmitMessage }>
<Icon src = { IconPlane } />
</div>
</div>
</div>
<div className = 'usrmsg-form'>
<TextareaAutosize
id = 'usermsg'
inputRef = { this._setTextAreaRef }
maxRows = { 5 }
onChange = { this._onMessageChange }
onHeightChange = { this.props.onResize }
onKeyDown = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') }
value = { this.state.message } />
</div>
</div>
);
@@ -148,6 +159,24 @@ class ChatInput extends Component<Props, State> {
this._textArea && this._textArea.focus();
}
_onSubmitMessage: () => void;
/**
* Submits the message to the chat window.
*
* @returns {void}
*/
_onSubmitMessage() {
const trimmed = this.state.message.trim();
if (trimmed) {
this.props.onSend(trimmed);
this.setState({ message: '' });
}
}
_onDetectSubmit: (Object) => void;
/**
@@ -163,13 +192,7 @@ class ChatInput extends Component<Props, State> {
&& event.shiftKey === false) {
event.preventDefault();
const trimmed = this.state.message.trim();
if (trimmed) {
this.props.onSend(trimmed);
this.setState({ message: '' });
}
this._onSubmitMessage();
}
}

View File

@@ -8,6 +8,8 @@ import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { updateSettings } from '../../../base/settings';
import KeyboardAvoider from './KeyboardAvoider';
/**
* The type of the React {@code Component} props of {@DisplayNameForm}.
*/
@@ -70,16 +72,24 @@ class DisplayNameForm extends Component<Props, State> {
return (
<div id = 'nickname'>
<span>{ this.props.t('chat.nickname.title') }</span>
<form onSubmit = { this._onSubmit }>
<FieldTextStateless
autoFocus = { true }
compact = { true }
id = 'nickinput'
label = { t('chat.nickname.title') }
onChange = { this._onDisplayNameChange }
placeholder = { t('chat.nickname.popover') }
shouldFitContainer = { true }
type = 'text'
value = { this.state.displayName } />
</form>
<div
className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` }
onClick = { this._onSubmit }>
{ t('chat.enter') }
</div>
<KeyboardAvoider />
</div>
);
}

View File

@@ -0,0 +1,60 @@
// @flow
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { isIosMobileBrowser } from '../../../base/environment/utils';
const Avoider = styled.div`
height: ${props => props.elementHeight}px;
`;
/**
* Component that renders an element to lift the chat input above the Safari keyboard,
* computing the appropriate height comparisons based on the {@code visualViewport}.
*
* @returns {ReactElement}
*/
function KeyboardAvoider() {
if (!isIosMobileBrowser()) {
return null;
}
const [ elementHeight, setElementHeight ] = useState(0);
const [ storedHeight, setStoredHeight ] = useState(window.innerHeight);
/**
* Handles the resizing of the visual viewport in order to compute
* the {@code KeyboardAvoider}'s height.
*
* @returns {void}
*/
function handleViewportResize() {
const { innerWidth, visualViewport: { width, height } } = window;
// Compare the widths to make sure the {@code visualViewport} didn't resize due to zooming.
if (width === innerWidth) {
if (height < storedHeight) {
setElementHeight(storedHeight - height);
} else {
setElementHeight(0);
}
setStoredHeight(height);
}
}
useEffect(() => {
// Call the handler in case the keyboard is open when the {@code KeyboardAvoider} is mounted.
handleViewportResize();
window.visualViewport.addEventListener('resize', handleViewportResize);
return () => {
window.visualViewport.removeEventListener('resize', handleViewportResize);
};
}, []);
return <Avoider elementHeight = { elementHeight } />;
}
export default KeyboardAvoider;

View File

@@ -0,0 +1,67 @@
// @flow
import React, { useEffect, useRef } from 'react';
import { isMobileBrowser } from '../../../base/environment/utils';
type Props = {
/**
* The component(s) that need to be scrollable on mobile.
*/
children: React$Node,
/**
* Whether the component is rendered within a modal.
*/
isModal: boolean
};
/**
* Component that disables {@code touchmove} propagation below it.
*
* @returns {ReactElement}
*/
function TouchmoveHack({ children, isModal }: Props) {
if (!isModal || !isMobileBrowser()) {
return children;
}
const touchMoveElementRef = useRef(null);
/**
* Atlaskit's {@code Modal} uses a third party library to disable touchmove events
* which makes scrolling inside dialogs impossible. We therefore employ this hack
* to intercept and stop the propagation of touchmove events from this wrapper that
* is placed around the chat conversation from the {@code ChatDialog}.
*
* @param {Event} event - The touchmove event fired within the component.
* @returns {void}
*/
function handleTouchMove(event: TouchEvent) {
event.stopImmediatePropagation();
}
useEffect(() => {
if (touchMoveElementRef && touchMoveElementRef.current) {
touchMoveElementRef.current.addEventListener('touchmove', handleTouchMove, true);
}
return () => {
if (touchMoveElementRef && touchMoveElementRef.current) {
touchMoveElementRef.current.removeEventListener('touchmove', handleTouchMove, true);
}
};
}, []);
return (
<div
className = 'touchmove-hack'
ref = { touchMoveElementRef }>
{children}
</div>
);
}
export default TouchmoveHack;

View File

@@ -58,6 +58,14 @@ const DEFAULT_STATE = {
*/
defaultBranding: true,
/**
* Url for a custom page for DID numbers list.
*
* @public
* @type {string}
*/
didPageUrl: '',
/**
* The custom invite domain.
*
@@ -101,6 +109,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
backgroundColor,
backgroundImageUrl,
defaultBranding,
didPageUrl,
inviteDomain,
logoClickUrl,
logoImageUrl
@@ -110,6 +119,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
backgroundColor,
backgroundImageUrl,
defaultBranding,
didPageUrl,
inviteDomain,
logoClickUrl,
logoImageUrl,

View File

@@ -1,14 +1,12 @@
// @flow
import React, { useState, useEffect } from 'react';
import React, { useEffect } from 'react';
import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
import { getRoomName } from '../../../../base/conference';
import { getInviteURL } from '../../../../base/connection';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { getLocalParticipant } from '../../../../base/participants';
import { connect } from '../../../../base/redux';
import { isVpaasMeeting } from '../../../../billing-counter/functions';
import EmbedMeetingTrigger from '../../../../embed-meeting/components/EmbedMeetingTrigger';
@@ -26,11 +24,6 @@ declare var interfaceConfig: Object;
type Props = {
/**
* The name of the current conference. Used as part of inviting users.
*/
_conferenceName: string,
/**
* The object representing the dialIn feature.
*/
@@ -41,6 +34,11 @@ type Props = {
*/
_embedMeetingVisible: boolean,
/**
* The meeting invitation text.
*/
_invitationText: string,
/**
* Whether or not invite contacts should be visible.
*/
@@ -57,14 +55,9 @@ type Props = {
_liveStreamViewURL: string,
/**
* The redux representation of the local participant.
* The default phone number.
*/
_localParticipantName: ?string,
/**
* The current location url of the conference.
*/
_locationUrl: Object,
_phoneNumber: ?string,
/**
* Invoked to obtain translated strings.
@@ -83,17 +76,15 @@ type Props = {
* @returns {React$Element<any>}
*/
function AddPeopleDialog({
_conferenceName,
_dialIn,
_embedMeetingVisible,
_invitationText,
_inviteContactsVisible,
_inviteUrl,
_liveStreamViewURL,
_localParticipantName,
_locationUrl,
_phoneNumber,
t,
updateNumbers }: Props) {
const [ phoneNumber, setPhoneNumber ] = useState(undefined);
/**
* Updates the dial-in numbers.
@@ -119,27 +110,6 @@ function AddPeopleDialog({
};
}, []);
/**
* Updates the phone number in the state once the dial-in numbers are fetched.
*
* @returns {void}
*/
useEffect(() => {
if (!phoneNumber && _dialIn && _dialIn.numbers) {
setPhoneNumber(_getDefaultPhoneNumber(_dialIn.numbers));
}
}, [ _dialIn ]);
const invite = getInviteText({
_conferenceName,
_localParticipantName,
_inviteUrl,
_locationUrl,
_dialIn,
_liveStreamViewURL,
phoneNumber,
t
});
const inviteSubject = t('addPeople.inviteMoreMailSubject', {
appName: interfaceConfig.APP_NAME
});
@@ -156,7 +126,7 @@ function AddPeopleDialog({
<CopyMeetingLinkSection url = { _inviteUrl } />
<InviteByEmailSection
inviteSubject = { inviteSubject }
inviteText = { invite } />
inviteText = { _invitationText } />
{ _embedMeetingVisible && <EmbedMeetingTrigger /> }
<div className = 'invite-more-dialog separator' />
{
@@ -165,11 +135,7 @@ function AddPeopleDialog({
}
{
_dialIn.numbers
&& <DialInSection
conferenceName = { _conferenceName }
dialIn = { _dialIn }
locationUrl = { _locationUrl }
phoneNumber = { phoneNumber } />
&& <DialInSection phoneNumber = { _phoneNumber } />
}
</div>
</Dialog>
@@ -181,29 +147,32 @@ function AddPeopleDialog({
* {@code AddPeopleDialog} component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component.
* @private
* @returns {Props}
*/
function mapStateToProps(state) {
const localParticipant = getLocalParticipant(state);
function mapStateToProps(state, ownProps) {
const currentLiveStreamingSession
= getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
const { iAmRecorder } = state['features/base/config'];
const addPeopleEnabled = isAddPeopleEnabled(state);
const dialOutEnabled = isDialOutEnabled(state);
const hideInviteContacts = iAmRecorder || (!addPeopleEnabled && !dialOutEnabled);
const dialIn = state['features/invite'];
const phoneNumber = dialIn && dialIn.numbers ? _getDefaultPhoneNumber(dialIn.numbers) : undefined;
return {
_conferenceName: getRoomName(state),
_dialIn: state['features/invite'],
_dialIn: dialIn,
_embedMeetingVisible: !isVpaasMeeting(state),
_invitationText: getInviteText({ state,
phoneNumber,
t: ownProps.t }),
_inviteContactsVisible: interfaceConfig.ENABLE_DIAL_OUT && !hideInviteContacts,
_inviteUrl: getInviteURL(state),
_liveStreamViewURL:
currentLiveStreamingSession
&& currentLiveStreamingSession.liveStreamViewURL,
_localParticipantName: localParticipant?.name,
_locationUrl: state['features/base/connection'].locationURL
_phoneNumber: phoneNumber
};
}

View File

@@ -3,26 +3,22 @@
import React from 'react';
import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { getDialInfoPageURL } from '../../../functions';
import DialInNumber from './DialInNumber';
type Props = {
/**
* The name of the current conference. Used as part of inviting users.
*/
conferenceName: string,
/**
* The object representing the dialIn feature.
*/
dialIn: Object,
_dialIn: Object,
/**
* The current location url of the conference.
* The url of the page containing the dial-in numbers list.
*/
locationUrl: Object,
_dialInfoPageUrl: string,
/**
* The phone number to dial to begin the process of dialing into a
@@ -45,25 +41,19 @@ type Props = {
* @returns {null|ReactElement}
*/
function DialInSection({
conferenceName,
dialIn,
locationUrl,
_dialIn,
_dialInfoPageUrl,
phoneNumber,
t
}: Props) {
return (
<div className = 'invite-more-dialog dial-in-display'>
<DialInNumber
conferenceID = { dialIn.conferenceID }
conferenceID = { _dialIn.conferenceID }
phoneNumber = { phoneNumber } />
<a
className = 'more-numbers'
href = {
getDialInfoPageURL(
conferenceName,
locationUrl
)
}
href = { _dialInfoPageUrl }
rel = 'noopener noreferrer'
target = '_blank'>
{ t('info.moreNumbers') }
@@ -72,4 +62,20 @@ function DialInSection({
);
}
export default translate(DialInSection);
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code DialInLink} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_dialIn: state['features/invite'],
_dialInfoPageUrl: getDialInfoPageURL(state)
};
}
export default translate(connect(_mapStateToProps)(DialInSection));

View File

@@ -1,7 +1,11 @@
// @flow
import { getActiveSession } from '../../features/recording/functions';
import { getRoomName } from '../base/conference';
import { getInviteURL } from '../base/connection';
import { i18next } from '../base/i18n';
import { isLocalParticipantModerator } from '../base/participants';
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants';
import { toState } from '../base/redux';
import { doGetJSON, parseURIString } from '../base/util';
import { isVpaasMeeting } from '../billing-counter/functions';
@@ -239,43 +243,44 @@ export function getInviteResultsForQuery(
* @returns {string}
*/
export function getInviteText({
_conferenceName,
_localParticipantName,
_inviteUrl,
_locationUrl,
_dialIn,
_liveStreamViewURL,
state,
phoneNumber,
t
}: Object) {
const inviteURL = _decodeRoomURI(_inviteUrl);
const dialIn = state['features/invite'];
const inviteUrl = getInviteURL(state);
const currentLiveStreamingSession = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
const liveStreamViewURL
= currentLiveStreamingSession
&& currentLiveStreamingSession.liveStreamViewURL;
const localParticipant = getLocalParticipant(state);
const localParticipantName = localParticipant?.name;
let invite = _localParticipantName
? t('info.inviteURLFirstPartPersonal', { name: _localParticipantName })
const inviteURL = _decodeRoomURI(inviteUrl);
let invite = localParticipantName
? t('info.inviteURLFirstPartPersonal', { name: localParticipantName })
: t('info.inviteURLFirstPartGeneral');
invite += t('info.inviteURLSecondPart', {
url: inviteURL
});
if (_liveStreamViewURL) {
if (liveStreamViewURL) {
const liveStream = t('info.inviteLiveStream', {
url: _liveStreamViewURL
url: liveStreamViewURL
});
invite = `${invite}\n${liveStream}`;
}
if (shouldDisplayDialIn(_dialIn)) {
if (shouldDisplayDialIn(dialIn)) {
const dial = t('info.invitePhone', {
number: phoneNumber,
conferenceID: _dialIn.conferenceID
conferenceID: dialIn.conferenceID
});
const moreNumbers = t('info.invitePhoneAlternatives', {
url: getDialInfoPageURL(
_conferenceName,
_locationUrl
),
url: getDialInfoPageURL(state),
silentUrl: `${inviteURL}#config.startSilent=true`
});
@@ -514,9 +519,7 @@ export function getShareInfoText(
.catch(error =>
logger.error('Error fetching numbers or conferenceID', error))
.then(defaultDialInNumber => {
let dialInfoPageUrl = getDialInfoPageURL(
room,
state['features/base/connection'].locationURL);
let dialInfoPageUrl = getDialInfoPageURL(state);
if (useHtml) {
dialInfoPageUrl
@@ -537,28 +540,19 @@ export function getShareInfoText(
/**
* Generates the URL for the static dial in info page.
*
* @param {string} conferenceName - The conference name.
* @param {Object} locationURL - The current location URL, the object coming
* from state ['features/base/connection'].locationURL.
* @param {Object} state - The state from the Redux store.
* @returns {string}
*/
export function getDialInfoPageURL(
conferenceName: string,
locationURL: Object) {
const origin = locationURL.origin;
const pathParts = locationURL.pathname.split('/');
export function getDialInfoPageURL(state: Object) {
const { didPageUrl } = state['features/dynamic-branding'];
const conferenceName = getRoomName(state);
const { locationURL } = state['features/base/connection'];
const { href } = locationURL;
const room = _decodeRoomURI(conferenceName);
pathParts.length = pathParts.length - 1;
const url = didPageUrl || `${href.substring(0, href.lastIndexOf('/'))}/static/dialInInfo.html`;
const newPath = pathParts.reduce((accumulator, currentValue) => {
if (currentValue) {
return `${accumulator}/${currentValue}`;
}
return accumulator;
}, '');
return `${origin}${newPath}/static/dialInInfo.html?room=${_decodeRoomURI(conferenceName)}`;
return `${url}?room=${room}`;
}
/**

View File

@@ -6,7 +6,7 @@ import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/pa
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { isTestModeEnabled } from '../base/testing';
import { NOTIFICATION_TYPE, showNotification } from '../notifications';
import { isPrejoinPageEnabled } from '../prejoin/functions';
import { shouldAutoKnock } from '../prejoin/functions';
import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes';
import {
@@ -100,8 +100,7 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
dispatch(openLobbyScreen());
if (isPrejoinPageEnabled(state) && !state['features/lobby'].knocking) {
// prejoin is enabled, so we knock automatically
if (shouldAutoKnock(state)) {
dispatch(startKnocking());
}

View File

@@ -11,22 +11,16 @@ import { getDialInfoPageURL, shouldDisplayDialIn } from '../../invite';
*/
type Props = {
/**
* The name of the current conference.
*/
_room: string,
/**
* The current location url of the conference.
*/
_locationURL: string,
/**
* The redux state representing the dial-in numbers feature.
*/
_dialIn: Object,
/**
* The url of the page containing the dial-in numbers list.
*/
_dialInfoPageUrl: string,
/**
* Invoked to obtain translated strings.
*/
@@ -47,7 +41,7 @@ class DialInLink extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _room, _locationURL, _dialIn, t } = this.props;
const { _dialIn, _dialInfoPageUrl, t } = this.props;
if (!shouldDisplayDialIn(_dialIn)) {
return null;
@@ -56,12 +50,7 @@ class DialInLink extends Component<Props> {
return (
<div>{t('toolbar.noAudioSignalDialInDesc')}&nbsp;
<a
href = {
getDialInfoPageURL(
_room,
_locationURL
)
}
href = { _dialInfoPageUrl }
rel = 'noopener noreferrer'
target = '_blank'>
{t('toolbar.noAudioSignalDialInLinkDesc')}
@@ -77,18 +66,12 @@ class DialInLink extends Component<Props> {
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _room: string,
* _locationURL: string,
* _dialIn: Object,
* }}
*/
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_room: state['features/base/conference'].room,
_locationURL: state['features/base/connection'].locationURL,
_dialIn: state['features/invite']
_dialIn: state['features/invite'],
_dialInfoPageUrl: getDialInfoPageURL(state)
};
}

View File

@@ -14,6 +14,17 @@
export const MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED
= 'MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED';
/**
* The type of the Redux action which signals that the overlay for slow gUM is visible or not.
*
* {
* type: TOGGLE_SLOW_GUM_OVERLAY,
* isVisible: {boolean},
* }
* @public
*/
export const TOGGLE_SLOW_GUM_OVERLAY = 'TOGGLE_SLOW_GUM_OVERLAY';
/**
* Adjust the state of the fatal error which shows/hides the reload screen. See
* action methods's description for more info about each of the fields.

View File

@@ -2,7 +2,8 @@
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR
SET_FATAL_ERROR,
TOGGLE_SLOW_GUM_OVERLAY
} from './actionTypes';
/**
@@ -26,6 +27,24 @@ export function mediaPermissionPromptVisibilityChanged(isVisible: boolean, brows
};
}
/**
* Signals that the prompt for media permission is visible or not.
*
* @param {boolean} isVisible - If the value is true - the prompt for media
* permission is visible otherwise the value is false/undefined.
* @public
* @returns {{
* type: SLOW_GET_USER_MEDIA_OVERLAY,
* isVisible: {boolean}
* }}
*/
export function toggleSlowGUMOverlay(isVisible: boolean) {
return {
type: TOGGLE_SLOW_GUM_OVERLAY,
isVisible
};
}
/**
* The action indicates that an unrecoverable error has occurred and the reload
* screen will be displayed or hidden.

View File

@@ -0,0 +1,33 @@
// @flow
import { Component } from 'react';
/**
* The type of the React {@code Component} props of
* {@link AbstractSlowGUMOverlay}.
*/
type Props = {
/**
* The function to translate human-readable text.
*/
t: Function
};
/**
* Implements a React {@link Component} for slow gUM overlay. Shown when
* a slow gUM promise resolution is detected
*/
export default class AbstractSlowGUMOverlay extends Component<Props> {
/**
* Determines whether this overlay needs to be rendered (according to a
* specific redux state). Called by {@link OverlayContainer}.
*
* @param {Object} state - The redux state.
* @returns {boolean} - If this overlay needs to be rendered, {@code true};
* {@code false}, otherwise.
*/
static needsRender(state: Object) {
return state['features/overlay'].isSlowGUMOverlayVisible;
}
}

View File

@@ -0,0 +1,36 @@
// @flow
import Spinner from '@atlaskit/spinner';
import React from 'react';
import { translate } from '../../../base/i18n';
import AbstractSlowGUMOverlay from './AbstractSlowGUMOverlay';
import OverlayFrame from './OverlayFrame';
/**
* Implements a React {@link Component} for slow gUM overlay. Shown when
* a slow gUM promise resolution is detected
*/
class SlowGUMOverlay extends AbstractSlowGUMOverlay {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
// const { t } = this.props;
return (
<OverlayFrame>
<div className = { 'overlay__spinner-container' }>
<Spinner
invertColor = { true }
size = { 'large' } />
</div>
</OverlayFrame>
);
}
}
export default translate(SlowGUMOverlay);

View File

@@ -5,3 +5,4 @@ export { default as OverlayFrame } from './OverlayFrame';
export { default as PageReloadOverlay } from './PageReloadOverlay';
export { default as SuspendedOverlay } from './SuspendedOverlay';
export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay';
export { default as SlowGUMOverlay } from './SlowGUMOverlay';

View File

@@ -2,6 +2,7 @@
import {
PageReloadOverlay,
SlowGUMOverlay,
SuspendedOverlay,
UserMediaPermissionsOverlay
} from './components/web';
@@ -17,6 +18,7 @@ export function getOverlays(): Array<Object> {
return [
PageReloadOverlay,
SuspendedOverlay,
UserMediaPermissionsOverlay
UserMediaPermissionsOverlay,
SlowGUMOverlay
];
}

View File

@@ -5,7 +5,8 @@ import { assign, ReducerRegistry, set } from '../base/redux';
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR
SET_FATAL_ERROR,
TOGGLE_SLOW_GUM_OVERLAY
} from './actionTypes';
/**
@@ -28,6 +29,8 @@ ReducerRegistry.register('features/overlay', (state = { }, action) => {
case SET_FATAL_ERROR:
return _setFatalError(state, action);
case TOGGLE_SLOW_GUM_OVERLAY:
return _toggleSlowGUMOverlay(state, action);
}
return state;
@@ -52,6 +55,24 @@ function _mediaPermissionPromptVisibilityChanged(
});
}
/**
* Reduces a specific redux action TOGGLE_SLOW_GUM_OVERLAY of
* the feature overlay.
*
* @param {Object} state - The redux state of the feature overlay.
* @param {Action} action - The redux action to reduce.
* @private
* @returns {Object} The new state of the feature overlay after the reduction of
* the specified action.
*/
function _toggleSlowGUMOverlay(
state,
{ isVisible }) {
return assign(state, {
isSlowGUMOverlayVisible: isVisible
});
}
/**
* Sets the {@code LoadConfigOverlay} overlay visible or not.
*

View File

@@ -4,7 +4,6 @@ declare var JitsiMeetJS: Object;
import uuid from 'uuid';
import { getRoomName } from '../base/conference';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { createLocalTrack } from '../base/lib-jitsi-meet';
import {
@@ -262,10 +261,7 @@ export function makePrecallTest(conferenceOptions: Object) {
*/
export function openDialInPage() {
return function(dispatch: Function, getState: Function) {
const state = getState();
const locationURL = state['features/base/connection'].locationURL;
const roomName = getRoomName(state);
const dialInPage = getDialInfoPageURL(roomName, locationURL);
const dialInPage = getDialInfoPageURL(getState());
openURLInBrowser(dialInPage, true);
};

View File

@@ -162,3 +162,16 @@ export function isPrejoinPageEnabled(state: Object): boolean {
export function isPrejoinPageVisible(state: Object): boolean {
return isPrejoinPageEnabled(state) && state['features/prejoin']?.showPrejoin;
}
/**
* Returns true if we should auto-knock in case lobby is enabled for the room.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function shouldAutoKnock(state: Object): boolean {
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
return (isPrejoinPageEnabled(state) || (iAmRecorder && iAmSipGateway))
&& !state['features/lobby'].knocking;
}

View File

@@ -7,8 +7,8 @@ import createTFLiteModule from './vendor/tflite/tflite';
import createTFLiteSIMDModule from './vendor/tflite/tflite-simd';
const models = {
'96': '/libs/segm_lite_v681.tflite',
'144': '/libs/segm_full_v679.tflite'
'96': 'libs/segm_lite_v681.tflite',
'144': 'libs/segm_full_v679.tflite'
};
/**
@@ -34,6 +34,10 @@ export async function createBlurEffect() {
models['144']
);
if (!modelResponse.ok) {
throw new Error('Failed to download tflite model!');
}
const model = await modelResponse.arrayBuffer();
tflite.HEAPU8.set(new Uint8Array(model), modelBufferOffset);

View File

@@ -216,7 +216,7 @@ function onConnectionSuccess() {
devices.push('video');
}
if (autoCreateLocalAudio) {
if (localAudio || autoCreateLocalAudio) {
devices.push('audio');
}