mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-31 20:02:27 +00:00
Compare commits
13 Commits
pr-testing
...
release-80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13d3ec9f6d | ||
|
|
f7acc2e15f | ||
|
|
a2c09357ca | ||
|
|
3ed68fdf1a | ||
|
|
df010e6a1d | ||
|
|
c901c18e21 | ||
|
|
d57b48f8d7 | ||
|
|
c549c4ce5b | ||
|
|
0951a9dd9a | ||
|
|
133e36ead6 | ||
|
|
914ad85baa | ||
|
|
70c3a2b01c | ||
|
|
0ce193e5c3 |
124
conference.js
124
conference.js
@@ -6,7 +6,7 @@ import Logger from '@jitsi/logger';
|
||||
import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
|
||||
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
|
||||
import Recorder from './modules/recorder/Recorder';
|
||||
import { createTaskQueue } from './modules/util/helpers';
|
||||
import { createDeferred, createTaskQueue } from './modules/util/helpers';
|
||||
import {
|
||||
createDeviceChangedEvent,
|
||||
createScreenSharingEvent,
|
||||
@@ -168,6 +168,8 @@ import { muteLocal } from './react/features/video-menu/actions.any';
|
||||
const logger = Logger.getLogger(__filename);
|
||||
let room;
|
||||
|
||||
const initPromise = createDeferred();
|
||||
|
||||
/*
|
||||
* Logic to open a desktop picker put on the window global for
|
||||
* lib-jitsi-meet to detect and invoke
|
||||
@@ -509,49 +511,56 @@ export default {
|
||||
},
|
||||
|
||||
startConference(tracks) {
|
||||
tracks.forEach(track => {
|
||||
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|
||||
|| (track.isVideoTrack() && this.isLocalVideoMuted())) {
|
||||
const mediaType = track.getType();
|
||||
logger.debug('startConference called!');
|
||||
|
||||
sendAnalytics(
|
||||
createTrackMutedEvent(mediaType, 'initial mute'));
|
||||
logger.log(`${mediaType} mute: initially muted.`);
|
||||
track.mute();
|
||||
const promise = initPromise?.promise || Promise.resolve();
|
||||
|
||||
return promise.then(() => {
|
||||
logger.debug('startConference start execution!');
|
||||
tracks.forEach(track => {
|
||||
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|
||||
|| (track.isVideoTrack() && this.isLocalVideoMuted())) {
|
||||
const mediaType = track.getType();
|
||||
|
||||
sendAnalytics(
|
||||
createTrackMutedEvent(mediaType, 'initial mute'));
|
||||
logger.log(`${mediaType} mute: initially muted.`);
|
||||
track.mute();
|
||||
}
|
||||
});
|
||||
|
||||
this._createRoom(tracks);
|
||||
|
||||
// if user didn't give access to mic or camera or doesn't have
|
||||
// them at all, we mark corresponding toolbar buttons as muted,
|
||||
// so that the user can try unmute later on and add audio/video
|
||||
// to the conference
|
||||
if (!tracks.find(t => t.isAudioTrack())) {
|
||||
this.updateAudioIconEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
this._createRoom(tracks);
|
||||
if (!tracks.find(t => t.isVideoTrack())) {
|
||||
this.setVideoMuteStatus();
|
||||
}
|
||||
|
||||
// if user didn't give access to mic or camera or doesn't have
|
||||
// them at all, we mark corresponding toolbar buttons as muted,
|
||||
// so that the user can try unmute later on and add audio/video
|
||||
// to the conference
|
||||
if (!tracks.find(t => t.isAudioTrack())) {
|
||||
this.updateAudioIconEnabled();
|
||||
}
|
||||
if (config.iAmRecorder) {
|
||||
this.recorder = new Recorder();
|
||||
}
|
||||
|
||||
if (!tracks.find(t => t.isVideoTrack())) {
|
||||
this.setVideoMuteStatus();
|
||||
}
|
||||
if (config.startSilent) {
|
||||
sendAnalytics(createStartSilentEvent());
|
||||
APP.store.dispatch(showNotification({
|
||||
descriptionKey: 'notify.startSilentDescription',
|
||||
titleKey: 'notify.startSilentTitle'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
}
|
||||
|
||||
if (config.iAmRecorder) {
|
||||
this.recorder = new Recorder();
|
||||
}
|
||||
|
||||
if (config.startSilent) {
|
||||
sendAnalytics(createStartSilentEvent());
|
||||
APP.store.dispatch(showNotification({
|
||||
descriptionKey: 'notify.startSilentDescription',
|
||||
titleKey: 'notify.startSilentTitle'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
}
|
||||
|
||||
// XXX The API will take care of disconnecting from the XMPP
|
||||
// server (and, thus, leaving the room) on unload.
|
||||
return new Promise((resolve, reject) => {
|
||||
new ConferenceConnector(resolve, reject, this).connect();
|
||||
});
|
||||
// XXX The API will take care of disconnecting from the XMPP
|
||||
// server (and, thus, leaving the room) on unload.
|
||||
return new Promise((resolve, reject) => {
|
||||
new ConferenceConnector(resolve, reject, this).connect();
|
||||
});
|
||||
}).catch(logger.error);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -618,7 +627,14 @@ export default {
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
|
||||
|
||||
return APP.store.dispatch(initPrejoin(localTracks, errors));
|
||||
const promise = APP.store.dispatch(initPrejoin(localTracks, errors));
|
||||
|
||||
promise.finally(() => {
|
||||
logger.debug('Resolving initPromise! prejoinVisible=true');
|
||||
initPromise.resolve();
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
logger.debug('Prejoin screen no longer displayed at the time when tracks were created');
|
||||
@@ -629,12 +645,19 @@ export default {
|
||||
|
||||
setGUMPendingStateOnFailedTracks(tracks, APP.store.dispatch);
|
||||
|
||||
return this._setLocalAudioVideoStreams(tracks);
|
||||
const promise = this._setLocalAudioVideoStreams(tracks);
|
||||
|
||||
promise.finally(() => {
|
||||
logger.debug('Resolving initPromise! Prejoin was visible initially but now it is hidden.');
|
||||
initPromise.resolve();
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
|
||||
|
||||
return Promise.all([
|
||||
const promise = Promise.all([
|
||||
tryCreateLocalTracks.then(tr => {
|
||||
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
|
||||
|
||||
@@ -649,7 +672,14 @@ export default {
|
||||
return filteredTracks;
|
||||
}),
|
||||
APP.store.dispatch(connect())
|
||||
]).then(([ tracks, _ ]) => {
|
||||
]);
|
||||
|
||||
promise.finally(() => {
|
||||
logger.debug('Resolving initPromise! prejoinVisible=false');
|
||||
initPromise.resolve();
|
||||
});
|
||||
|
||||
return promise.then(([ tracks, _ ]) => {
|
||||
this.startConference(tracks).catch(logger.error);
|
||||
});
|
||||
},
|
||||
@@ -705,11 +735,13 @@ export default {
|
||||
|
||||
/**
|
||||
* Simulates toolbar button click for audio mute. Used by shortcuts and API.
|
||||
*
|
||||
* @param {boolean} mute true for mute and false for unmute.
|
||||
* @param {boolean} [showUI] when set to false will not display any error
|
||||
* dialogs in case of media permissions error.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
muteAudio(mute, showUI = true) {
|
||||
async muteAudio(mute, showUI = true) {
|
||||
const state = APP.store.getState();
|
||||
|
||||
if (!mute
|
||||
@@ -749,7 +781,8 @@ export default {
|
||||
};
|
||||
|
||||
APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO ], IGUMPendingState.PENDING_UNMUTE));
|
||||
createLocalTracksF({ devices: [ 'audio' ] })
|
||||
|
||||
await createLocalTracksF({ devices: [ 'audio' ] })
|
||||
.then(([ audioTrack ]) => audioTrack)
|
||||
.catch(error => {
|
||||
maybeShowErrorDialog(error);
|
||||
@@ -1277,8 +1310,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(
|
||||
replaceLocalTrack(oldTrack, newTrack, room))
|
||||
APP.store.dispatch(replaceLocalTrack(oldTrack, newTrack, room))
|
||||
.then(() => {
|
||||
this.updateAudioIconEnabled();
|
||||
})
|
||||
|
||||
@@ -1736,8 +1736,6 @@ var config = {
|
||||
// tileTime: 5000,
|
||||
// // Limit results by rating: g, pg, pg-13, r. Default value: g.
|
||||
// rating: 'pg',
|
||||
// // The proxy server url for giphy requests in the web app.
|
||||
// proxyUrl: 'https://giphy-proxy.example.com',
|
||||
// },
|
||||
|
||||
// Logging
|
||||
|
||||
@@ -443,7 +443,7 @@
|
||||
"shareVideoTitle": "Share video",
|
||||
"shareYourScreen": "Share your screen",
|
||||
"shareYourScreenDisabled": "Screen sharing disabled.",
|
||||
"sharedVideoDialogError": "Error: Invalid URL",
|
||||
"sharedVideoDialogError": "Error: Invalid or forbidden URL",
|
||||
"sharedVideoLinkPlaceholder": "YouTube link or direct video link",
|
||||
"show": "Show",
|
||||
"start": "Start ",
|
||||
@@ -1486,6 +1486,12 @@
|
||||
},
|
||||
"visitors": {
|
||||
"chatIndicator": "(visitor)",
|
||||
"joinMeeting": {
|
||||
"description": "You're currently an observer in this conference.",
|
||||
"raiseHand": "Raise your hand",
|
||||
"title": "Joining meeting",
|
||||
"wishToSpeak": "If you wish to speak, please raise your hand below and wait for the moderator's approval."
|
||||
},
|
||||
"labelTooltip": "Number of visitors: {{count}}",
|
||||
"notification": {
|
||||
"demoteDescription": "Sent here by {{actor}}, raise your hand to participate",
|
||||
|
||||
@@ -100,7 +100,7 @@ import {
|
||||
} from '../../react/features/participants-pane/actions';
|
||||
import { getParticipantsPaneOpen, isForceMuted } from '../../react/features/participants-pane/functions';
|
||||
import { startLocalVideoRecording, stopLocalVideoRecording } from '../../react/features/recording/actions.any';
|
||||
import { RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||
import { getActiveSession, supportsLocalRecording } from '../../react/features/recording/functions';
|
||||
import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/features/screen-share/actions';
|
||||
import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
|
||||
@@ -629,6 +629,7 @@ function initCommands() {
|
||||
* @param { string } arg.youtubeStreamKey - The youtube stream key.
|
||||
* @param { string } arg.youtubeBroadcastID - The youtube broadcast ID.
|
||||
* @param { Object } arg.extraMetadata - Any extra metadata params for file recording.
|
||||
* @param { boolean } arg.transcription - Whether a transcription should be started or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
'start-recording': ({
|
||||
@@ -640,7 +641,8 @@ function initCommands() {
|
||||
rtmpBroadcastID,
|
||||
youtubeStreamKey,
|
||||
youtubeBroadcastID,
|
||||
extraMetadata = {}
|
||||
extraMetadata = {},
|
||||
transcription
|
||||
}) => {
|
||||
const state = APP.store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
@@ -715,25 +717,33 @@ function initCommands() {
|
||||
mode: JitsiRecordingConstants.mode.STREAM,
|
||||
streamId: youtubeStreamKey || rtmpStreamKey
|
||||
};
|
||||
} else {
|
||||
logger.error('Invalid recording mode provided');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isScreenshotCaptureEnabled(state, true, false)) {
|
||||
APP.store.dispatch(toggleScreenshotCaptureSummary(true));
|
||||
}
|
||||
conference.startRecording(recordingConfig);
|
||||
|
||||
// Start audio / video recording, if requested.
|
||||
if (typeof recordingConfig !== 'undefined') {
|
||||
conference.startRecording(recordingConfig);
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
APP.store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops a recording or streaming in progress.
|
||||
*
|
||||
* @param {string} mode - `local`, `file` or `stream`.
|
||||
* @param {boolean} transcription - Whether the transcription needs to be stopped.
|
||||
* @returns {void}
|
||||
*/
|
||||
'stop-recording': mode => {
|
||||
'stop-recording': (mode, transcription) => {
|
||||
const state = APP.store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
@@ -743,6 +753,13 @@ function initCommands() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
APP.store.dispatch(setRequestingSubtitles(false, false, null));
|
||||
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: false
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === 'local') {
|
||||
APP.store.dispatch(stopLocalVideoRecording());
|
||||
|
||||
@@ -1917,14 +1934,16 @@ class API {
|
||||
* @param {boolean} on - True if recording is on, false otherwise.
|
||||
* @param {string} mode - Stream or file or local.
|
||||
* @param {string} error - Error type or null if success.
|
||||
* @param {boolean} transcription - True if a transcription is being recorded, false otherwise.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyRecordingStatusChanged(on, mode, error) {
|
||||
notifyRecordingStatusChanged(on, mode, error, transcription) {
|
||||
this._sendEvent({
|
||||
name: 'recording-status-changed',
|
||||
on,
|
||||
mode,
|
||||
error
|
||||
error,
|
||||
transcription
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
6
modules/API/external/external_api.js
vendored
6
modules/API/external/external_api.js
vendored
@@ -1446,6 +1446,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* @param { string } options.youtubeStreamKey - The youtube stream key.
|
||||
* @param { string } options.youtubeBroadcastID - The youtube broadcast ID.
|
||||
* @param {Object } options.extraMetadata - Any extra metadata params for file recording.
|
||||
* @param { boolean } arg.transcription - Whether a transcription should be started or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
startRecording(options) {
|
||||
@@ -1456,10 +1457,11 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* Stops a recording or streaming session that is in progress.
|
||||
*
|
||||
* @param {string} mode - `file` or `stream`.
|
||||
* @param {boolean} transcription - Whether the transcription needs to be stopped.
|
||||
* @returns {void}
|
||||
*/
|
||||
stopRecording(mode) {
|
||||
this.executeCommand('stopRecording', mode);
|
||||
stopRecording(mode, transcription) {
|
||||
this.executeCommand('stopRecording', mode, transcription);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -374,7 +374,6 @@ export interface IConfig {
|
||||
giphy?: {
|
||||
displayMode?: 'all' | 'tile' | 'chat';
|
||||
enabled?: boolean;
|
||||
proxyUrl?: string;
|
||||
rating?: 'g' | 'pg' | 'pg-13' | 'r';
|
||||
sdkKey?: string;
|
||||
tileTime?: number;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isTrackStreamingStatusInactive
|
||||
} from '../../../connection-indicator/functions';
|
||||
import SharedVideo from '../../../shared-video/components/native/SharedVideo';
|
||||
import { isSharedVideoEnabled } from '../../../shared-video/functions';
|
||||
import { IStateful } from '../../app/types';
|
||||
import Avatar from '../../avatar/components/Avatar';
|
||||
import { translate } from '../../i18n/functions';
|
||||
@@ -236,7 +237,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
_isConnectionInactive: isTrackStreamingStatusInactive(videoTrack),
|
||||
_isSharedVideoParticipant: isSharedVideoParticipant(participant),
|
||||
_participantName: getParticipantDisplayName(state, participantId),
|
||||
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo,
|
||||
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo
|
||||
&& isSharedVideoEnabled(state),
|
||||
_videoTrack: videoTrack
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import { toArray } from 'react-emoji-render';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import GifMessage from '../../../../chat/components/web/GifMessage';
|
||||
import { GIF_PREFIX } from '../../../../gifs/constants';
|
||||
import { isGifMessage } from '../../../../gifs/functions.web';
|
||||
import { isGifEnabled, isGifMessage } from '../../../../gifs/functions.web';
|
||||
|
||||
import Linkify from './Linkify';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Whether the gifs are enabled or not.
|
||||
*/
|
||||
gifEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The body of the message.
|
||||
*/
|
||||
@@ -43,11 +50,11 @@ class Message extends Component<IProps> {
|
||||
|
||||
// Tokenize the text in order to avoid emoji substitution for URLs
|
||||
const tokens = text ? text.split(' ') : [];
|
||||
|
||||
const content = [];
|
||||
const { gifEnabled } = this.props;
|
||||
|
||||
// check if the message is a GIF
|
||||
if (isGifMessage(text)) {
|
||||
if (gifEnabled && isGifMessage(text)) {
|
||||
const url = text.substring(GIF_PREFIX.length, text.length - 1);
|
||||
|
||||
content.push(<GifMessage
|
||||
@@ -93,4 +100,16 @@ class Message extends Component<IProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export default Message;
|
||||
/**
|
||||
* Maps part of the redux state to the props of this component.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
gifEnabled: isGifEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(Message);
|
||||
|
||||
@@ -111,12 +111,9 @@ const Dialog = ({
|
||||
}, [ onCancel ]);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (onSubmit && (
|
||||
(document.activeElement && !operatesWithEnterKey(document.activeElement))
|
||||
|| !document.activeElement
|
||||
)) {
|
||||
if ((document.activeElement && !operatesWithEnterKey(document.activeElement)) || !document.activeElement) {
|
||||
!disableAutoHideOnSubmit && dispatch(hideDialog());
|
||||
onSubmit();
|
||||
onSubmit?.();
|
||||
}
|
||||
}, [ onSubmit ]);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IReduxState } from '../../../app/types';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Linkify from '../../../base/react/components/native/Linkify';
|
||||
import { isGifMessage } from '../../../gifs/functions.native';
|
||||
import { isGifEnabled, isGifMessage } from '../../../gifs/functions.native';
|
||||
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
|
||||
import {
|
||||
getCanReplyToMessage,
|
||||
@@ -32,7 +32,7 @@ class ChatMessage extends Component<IChatMessageProps> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { message, knocking } = this.props;
|
||||
const { gifEnabled, message, knocking } = this.props;
|
||||
const localMessage = message.messageType === MESSAGE_TYPE_LOCAL;
|
||||
const { privateMessage, lobbyChat } = message;
|
||||
|
||||
@@ -82,7 +82,7 @@ class ChatMessage extends Component<IChatMessageProps> {
|
||||
<View style = { messageBubbleStyle }>
|
||||
<View style = { styles.textWrapper as ViewStyle } >
|
||||
{ this._renderDisplayName() }
|
||||
{ isGifMessage(messageText)
|
||||
{ gifEnabled && isGifMessage(messageText)
|
||||
? <GifMessage message = { messageText } />
|
||||
: (
|
||||
<Linkify
|
||||
@@ -210,6 +210,7 @@ class ChatMessage extends Component<IChatMessageProps> {
|
||||
function _mapStateToProps(state: IReduxState, { message }: IChatMessageProps) {
|
||||
return {
|
||||
canReply: getCanReplyToMessage(state, message),
|
||||
gifEnabled: isGifEnabled(state),
|
||||
knocking: state['features/lobby'].knocking
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
import { addGif } from '../gifs/actions';
|
||||
import { GIF_PREFIX } from '../gifs/constants';
|
||||
import { getGifDisplayMode, isGifMessage } from '../gifs/function.any';
|
||||
import { getGifDisplayMode, isGifEnabled, isGifMessage } from '../gifs/function.any';
|
||||
import { showMessageNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import { resetNbUnreadPollsMessages } from '../polls/actions';
|
||||
@@ -314,7 +314,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
function _onConferenceMessageReceived(store: IStore, { displayName, id, isGuest, message, timestamp, privateMessage }: {
|
||||
displayName?: string; id: string; isGuest?: boolean;
|
||||
message: string; privateMessage: boolean; timestamp: number; }) {
|
||||
const isGif = isGifMessage(message);
|
||||
const isGif = isGifEnabled(store.getState()) && isGifMessage(message);
|
||||
|
||||
if (isGif) {
|
||||
_handleGifMessageReceived(store, id, message);
|
||||
|
||||
@@ -44,6 +44,11 @@ export interface IChatMessageProps extends WithTranslation {
|
||||
*/
|
||||
canReply?: boolean;
|
||||
|
||||
/**
|
||||
* Whether gifs are enabled or not.
|
||||
*/
|
||||
gifEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* Whether current participant is currently knocking in the lobby room.
|
||||
*/
|
||||
|
||||
@@ -2,12 +2,11 @@ import React, { useCallback } from 'react';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { openHighlightDialog } from '../../../recording/actions.native';
|
||||
import HighlightButton from '../../../recording/components/Recording/native/HighlightButton';
|
||||
import RecordingLabel from '../../../recording/components/native/RecordingLabel';
|
||||
import { getActiveSession } from '../../../recording/functions';
|
||||
import { isLiveStreamingRunning } from '../../../recording/functions';
|
||||
import VisitorsCountLabel from '../../../visitors/components/native/VisitorsCountLabel';
|
||||
|
||||
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
|
||||
@@ -30,8 +29,7 @@ interface IProps {
|
||||
|
||||
const AlwaysOnLabels = ({ createOnPress }: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const isStreaming = useSelector((state: IReduxState) =>
|
||||
Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM)));
|
||||
const isStreaming = useSelector(isLiveStreamingRunning);
|
||||
const openHighlightDialogCallback = useCallback(() =>
|
||||
dispatch(openHighlightDialog()), [ dispatch ]);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import { sendMessage } from '../../../chat/actions.any';
|
||||
import { goBack } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { formatGifUrlMessage, getGifRating, getGifUrl, getGiphyProxyUrl } from '../../functions.native';
|
||||
import { formatGifUrlMessage, getGifRating, getGifUrl } from '../../functions.native';
|
||||
|
||||
import GifsMenuFooter from './GifsMenuFooter';
|
||||
import styles from './styles';
|
||||
@@ -19,8 +19,6 @@ const GifsMenu = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const rating = useSelector(getGifRating) as GiphyRating;
|
||||
const proxyUrl = useSelector(getGiphyProxyUrl);
|
||||
|
||||
const options = {
|
||||
mediaType: GiphyMediaType.Gif,
|
||||
limit: 20,
|
||||
@@ -35,7 +33,7 @@ const GifsMenu = () => {
|
||||
});
|
||||
|
||||
const sendGif = useCallback(e => {
|
||||
const url = getGifUrl(e.nativeEvent.media, proxyUrl);
|
||||
const url = getGifUrl(e.nativeEvent.media);
|
||||
|
||||
sendAnalytics(createGifSentEvent());
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GiphyFetch, TrendingOptions, setServerUrl } from '@giphy/js-fetch-api';
|
||||
import { GiphyFetch, TrendingOptions } from '@giphy/js-fetch-api';
|
||||
import { Grid } from '@giphy/react-components';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -20,8 +20,7 @@ import {
|
||||
formatGifUrlMessage,
|
||||
getGifAPIKey,
|
||||
getGifRating,
|
||||
getGifUrl,
|
||||
getGiphyProxyUrl
|
||||
getGifUrl
|
||||
} from '../../function.any';
|
||||
|
||||
const OVERFLOW_DRAWER_PADDING = 16;
|
||||
@@ -104,7 +103,6 @@ function GifsMenu({ columns = 2, parent }: IProps) {
|
||||
= parent === IReactionsMenuParent.OverflowDrawer || parent === IReactionsMenuParent.OverflowMenu;
|
||||
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
|
||||
const rating = useSelector(getGifRating);
|
||||
const proxyUrl = useSelector(getGiphyProxyUrl);
|
||||
|
||||
const fetchGifs = useCallback(async (offset = 0) => {
|
||||
const options: TrendingOptions = {
|
||||
@@ -126,7 +124,7 @@ function GifsMenu({ columns = 2, parent }: IProps) {
|
||||
|
||||
const handleGifClick = useCallback((gif, e) => {
|
||||
e?.stopPropagation();
|
||||
const url = getGifUrl(gif, proxyUrl);
|
||||
const url = getGifUrl(gif);
|
||||
|
||||
sendAnalytics(createGifSentEvent());
|
||||
batch(() => {
|
||||
@@ -189,12 +187,6 @@ function GifsMenu({ columns = 2, parent }: IProps) {
|
||||
// This fixes that.
|
||||
useEffect(() => setSearchKey(''), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (proxyUrl) {
|
||||
setServerUrl(proxyUrl);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onInputKeyPress = useCallback((e: React.KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
@@ -33,15 +33,6 @@ export function getGifRating(state: IReduxState) {
|
||||
return getGifConfig(state).rating || GIF_DEFAULT_RATING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Giphy proxy url.
|
||||
*
|
||||
* @param {IReduxState} state - Redux state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getGiphyProxyUrl(state: IReduxState) {
|
||||
return getGifConfig(state).proxyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL of the GIF for the given participant or null if there's none.
|
||||
@@ -54,6 +45,26 @@ export function getGifForParticipant(state: IReduxState, participantId: string):
|
||||
return isGifEnabled(state) ? state['features/gifs'].gifList.get(participantId) || {} : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a given URL is allowed to be rendered as gif and false otherwise.
|
||||
*
|
||||
* @param {string} url - The URL to be validated.
|
||||
* @returns {boolean} - True if a given URL is allowed to be rendered as gif and false otherwise.
|
||||
*/
|
||||
export function isGifUrlAllowed(url: string) {
|
||||
let hostname: string | undefined;
|
||||
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
|
||||
hostname = urlObject?.hostname;
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hostname === 'i.giphy.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the message is a GIF message.
|
||||
*
|
||||
@@ -61,26 +72,23 @@ export function getGifForParticipant(state: IReduxState, participantId: string):
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isGifMessage(message: string) {
|
||||
const url = message.substring(GIF_PREFIX.length, message.length - 1);
|
||||
|
||||
return message.trim().toLowerCase()
|
||||
.startsWith(GIF_PREFIX);
|
||||
.startsWith(GIF_PREFIX) && isGifUrlAllowed(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the gif selected in the gifs menu.
|
||||
*
|
||||
* @param {Object} gif - The gif data.
|
||||
* @param {string} proxyUrl - The proxy server url.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getGifUrl(gif?: { data?: { embed_url: string; }; embed_url?: string; }, proxyUrl?: string) {
|
||||
export function getGifUrl(gif?: { data?: { embed_url: string; }; embed_url?: string; }) {
|
||||
const embedUrl = gif?.embed_url || gif?.data?.embed_url || '';
|
||||
const idx = embedUrl.lastIndexOf('/');
|
||||
const id = embedUrl.substr(idx + 1);
|
||||
|
||||
if (proxyUrl) {
|
||||
return `${proxyUrl}gifs/id/${id}`;
|
||||
}
|
||||
|
||||
return `https://i.giphy.com/media/${id}/giphy.gif`;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,7 @@ const initGlobalKeyboardShortcuts = () =>
|
||||
helpCharacter: 'SPACE',
|
||||
helpDescription: 'keyboardShortcuts.pushToTalk',
|
||||
handler: () => {
|
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_RELEASED));
|
||||
logger.log('Talk shortcut released');
|
||||
APP.conference.muteAudio(true);
|
||||
// Handled directly on the global handler.
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -82,7 +80,18 @@ export const initKeyboardShortcuts = () =>
|
||||
(dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
dispatch(initGlobalKeyboardShortcuts());
|
||||
|
||||
window.onkeyup = (e: KeyboardEvent) => {
|
||||
const pttDelay = 50;
|
||||
let pttTimeout: number | undefined;
|
||||
|
||||
// Used to chain the push to talk operations in order to fix an issue when on press we actually need to create
|
||||
// a new track and the release happens before the track is created. In this scenario the release is ignored.
|
||||
// The chaining would also prevent creating multiple new tracks if the space bar is pressed and released
|
||||
// multiple times before the new track creation finish.
|
||||
// TODO: Revisit the fix once we have better track management in LJM. It is possible that we would not need the
|
||||
// chaining at all.
|
||||
let mutePromise = Promise.resolve();
|
||||
|
||||
window.addEventListener('keyup', (e: KeyboardEvent) => {
|
||||
const state = getState();
|
||||
const enabled = areKeyboardShortcutsEnabled(state);
|
||||
const shortcuts = getKeyboardShortcuts(state);
|
||||
@@ -93,12 +102,22 @@ export const initKeyboardShortcuts = () =>
|
||||
|
||||
const key = getKeyboardKey(e).toUpperCase();
|
||||
|
||||
if (key === ' ') {
|
||||
clearTimeout(pttTimeout);
|
||||
pttTimeout = window.setTimeout(() => {
|
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_RELEASED));
|
||||
logger.log('Talk shortcut released');
|
||||
mutePromise = mutePromise.then(() =>
|
||||
APP.conference.muteAudio(true).catch(() => { /* nothing to be done */ }));
|
||||
}, pttDelay);
|
||||
}
|
||||
|
||||
if (shortcuts.has(key)) {
|
||||
shortcuts.get(key)?.handler(e);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
window.onkeydown = (e: KeyboardEvent) => {
|
||||
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
const state = getState();
|
||||
const enabled = areKeyboardShortcutsEnabled(state);
|
||||
|
||||
@@ -110,11 +129,13 @@ export const initKeyboardShortcuts = () =>
|
||||
const key = getKeyboardKey(e).toUpperCase();
|
||||
|
||||
if (key === ' ' && !focusedElement) {
|
||||
clearTimeout(pttTimeout);
|
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_PRESSED));
|
||||
logger.log('Talk shortcut pressed');
|
||||
APP.conference.muteAudio(false);
|
||||
mutePromise = mutePromise.then(() =>
|
||||
APP.conference.muteAudio(false).catch(() => { /* nothing to be done */ }));
|
||||
} else if (key === 'ESCAPE') {
|
||||
focusedElement?.blur();
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -228,6 +228,8 @@ export function joinConference(options?: Object, ignoreJoiningInProgress = false
|
||||
dispatch(setJoiningInProgress(true));
|
||||
}
|
||||
|
||||
logger.debug('joinConference executed!');
|
||||
|
||||
options && dispatch(updateConfig(options));
|
||||
|
||||
dispatch(connect(jid, password)).then(async () => {
|
||||
|
||||
@@ -40,6 +40,11 @@ interface IProps extends AbstractButtonProps {
|
||||
*/
|
||||
_raisedHand: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the click is disabled.
|
||||
*/
|
||||
disableClick?: boolean;
|
||||
|
||||
/**
|
||||
* Used to close the overflow menu after raise hand is clicked.
|
||||
*/
|
||||
@@ -75,8 +80,14 @@ class RaiseHandButton extends Component<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { disableClick, onCancel } = this.props;
|
||||
|
||||
if (disableClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toggleRaisedHand();
|
||||
this.props.onCancel();
|
||||
onCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,4 +170,23 @@ function _mapStateToProps(state: IReduxState) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _standaloneMapStateToProps(state: IReduxState) {
|
||||
const _enabled = getFeatureFlag(state, RAISE_HAND_ENABLED, true);
|
||||
|
||||
return {
|
||||
_enabled
|
||||
};
|
||||
}
|
||||
|
||||
const StandaloneRaiseHandButton = translate(connect(_standaloneMapStateToProps)(RaiseHandButton));
|
||||
|
||||
export { StandaloneRaiseHandButton };
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RaiseHandButton));
|
||||
|
||||
@@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconRaiseHand } from '../../../base/icons/svg';
|
||||
import { raiseHand } from '../../../base/participants/actions';
|
||||
@@ -15,6 +15,16 @@ import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/too
|
||||
*/
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* Whether or not the click is disabled.
|
||||
*/
|
||||
disableClick?: boolean;
|
||||
|
||||
/**
|
||||
* Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Whether or not the hand is raised.
|
||||
*/
|
||||
@@ -51,7 +61,11 @@ class RaiseHandButton extends AbstractButton<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, raisedHand } = this.props;
|
||||
const { disableClick, dispatch, raisedHand } = this.props;
|
||||
|
||||
if (disableClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendAnalytics(createToolbarEvent(
|
||||
'raise.hand',
|
||||
@@ -76,4 +90,6 @@ const mapStateToProps = (state: IReduxState) => {
|
||||
};
|
||||
};
|
||||
|
||||
export { RaiseHandButton };
|
||||
|
||||
export default translate(connect(mapStateToProps)(RaiseHandButton));
|
||||
|
||||
@@ -5,8 +5,8 @@ import { IReduxState } from '../../app/types';
|
||||
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
|
||||
import { isRecorderTranscriptionsRunning } from '../../transcribing/functions';
|
||||
import {
|
||||
getActiveSession,
|
||||
getSessionStatusToShow,
|
||||
isLiveStreamingRunning,
|
||||
isRecordingRunning,
|
||||
isRemoteParticipantRecordingLocally
|
||||
} from '../functions';
|
||||
@@ -81,9 +81,9 @@ export function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { mode } = ownProps;
|
||||
const isLiveStreamingLabel = mode === JitsiRecordingConstants.mode.STREAM;
|
||||
const _isTranscribing = isRecorderTranscriptionsRunning(state);
|
||||
const isLivestreamingRunning = Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM));
|
||||
const _isLivestreamingRunning = isLiveStreamingRunning(state);
|
||||
const _isVisible = isLiveStreamingLabel
|
||||
? isLivestreamingRunning // this is the livestreaming label
|
||||
? _isLivestreamingRunning // this is the livestreaming label
|
||||
: isRecordingRunning(state) || isRemoteParticipantRecordingLocally(state)
|
||||
|| _isTranscribing; // this is the recording label
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@ import { IReduxState } from '../../../app/types';
|
||||
import { IconSites } from '../../../base/icons/svg';
|
||||
import { MEET_FEATURES } from '../../../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../../../base/jwt/functions';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { isLocalParticipantModerator } from '../../../base/participants/functions';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
|
||||
import { isRecorderTranscriptionsRunning } from '../../../transcribing/functions';
|
||||
import { getActiveSession, isCloudRecordingRunning, isLiveStreamingButtonVisible } from '../../functions';
|
||||
import { isCloudRecordingRunning, isLiveStreamingButtonVisible, isLiveStreamingRunning } from '../../functions';
|
||||
|
||||
import { getLiveStreaming } from './functions';
|
||||
|
||||
@@ -154,7 +153,7 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
|
||||
return {
|
||||
_disabled,
|
||||
_isLiveStreamRunning: Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM)),
|
||||
_isLiveStreamRunning: isLiveStreamingRunning(state),
|
||||
_tooltip,
|
||||
visible
|
||||
};
|
||||
|
||||
@@ -168,6 +168,16 @@ export function isCloudRecordingRunning(state: IReduxState) {
|
||||
return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a live streaming running.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state to search in.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLiveStreamingRunning(state: IReduxState) {
|
||||
return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a recording session running.
|
||||
*
|
||||
@@ -262,7 +272,7 @@ export function getRecordButtonProps(state: IReduxState) {
|
||||
}
|
||||
|
||||
// disable the button if the livestreaming is running.
|
||||
if (visible && getActiveSession(state, JitsiRecordingConstants.mode.STREAM)) {
|
||||
if (visible && isLiveStreamingRunning(state)) {
|
||||
disabled = true;
|
||||
tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
(recorderSession: any) => {
|
||||
if (recorderSession) {
|
||||
recorderSession.getID() && dispatch(updateRecordingSessionData(recorderSession));
|
||||
recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch);
|
||||
recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch, getState);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -133,7 +133,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
dispatch(updateLocalRecordingStatus(true, onlySelf));
|
||||
sendAnalytics(createRecordingEvent('started', `local${onlySelf ? '.self' : ''}`));
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRecordingStatusChanged(true, 'local');
|
||||
APP.API.notifyRecordingStatusChanged(
|
||||
true, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error('Capture failed', err);
|
||||
@@ -154,7 +155,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
};
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRecordingStatusChanged(false, 'local', err.message);
|
||||
APP.API.notifyRecordingStatusChanged(
|
||||
false, 'local', err.message, isRecorderTranscriptionsRunning(getState()));
|
||||
}
|
||||
|
||||
dispatch(showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
@@ -172,7 +174,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
dispatch(playSound(RECORDING_OFF_SOUND_ID));
|
||||
}
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRecordingStatusChanged(false, 'local');
|
||||
APP.API.notifyRecordingStatusChanged(
|
||||
false, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -237,7 +240,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
}
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRecordingStatusChanged(true, mode);
|
||||
APP.API.notifyRecordingStatusChanged(
|
||||
true, mode, undefined, isRecorderTranscriptionsRunning(state));
|
||||
}
|
||||
}
|
||||
} else if (updatedSessionData?.status === OFF && oldSessionData?.status !== OFF) {
|
||||
@@ -269,7 +273,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
}
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRecordingStatusChanged(false, mode);
|
||||
APP.API.notifyRecordingStatusChanged(
|
||||
false, mode, undefined, isRecorderTranscriptionsRunning(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,14 +317,15 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
* in recording session.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} recorderSession - The recorder session model from the
|
||||
* @param {Object} session - The recorder session model from the
|
||||
* lib.
|
||||
* @param {Dispatch} dispatch - The Redux Dispatch function.
|
||||
* @param {Function} getState - The Redux getState function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _showRecordingErrorNotification(recorderSession: any, dispatch: IStore['dispatch']) {
|
||||
const mode = recorderSession.getMode();
|
||||
const error = recorderSession.getError();
|
||||
function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const mode = session.getMode();
|
||||
const error = session.getError();
|
||||
const isStreamMode = mode === JitsiMeetJS.constants.recording.mode.STREAM;
|
||||
|
||||
switch (error) {
|
||||
@@ -367,6 +373,6 @@ function _showRecordingErrorNotification(recorderSession: any, dispatch: IStore[
|
||||
}
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRecordingStatusChanged(false, mode, error);
|
||||
APP.API.notifyRecordingStatusChanged(false, mode, error, isRecorderTranscriptionsRunning(getState()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getLocalParticipant } from '../base/participants/functions';
|
||||
|
||||
import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes';
|
||||
import { SharedVideoDialog } from './components';
|
||||
import { isSharedVideoEnabled, isURLAllowedForSharedVideo } from './functions';
|
||||
|
||||
/**
|
||||
* Resets the status of the shared video.
|
||||
@@ -89,6 +90,9 @@ export function stopSharedVideo() {
|
||||
*/
|
||||
export function playSharedVideo(videoUrl: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
if (!isSharedVideoEnabled(getState()) || !isURLAllowedForSharedVideo(videoUrl)) {
|
||||
return;
|
||||
}
|
||||
const conference = getCurrentConference(getState());
|
||||
|
||||
if (conference) {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
// @ts-ignore
|
||||
export { default as SharedVideoDialog } from './native/SharedVideoDialog';
|
||||
export { default as SharedVideoButton } from './native/SharedVideoButton';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as SharedVideoDialog } from './web/SharedVideoDialog';
|
||||
export { default as SharedVideoButton } from './web/SharedVideoButton';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IReduxState } from '../../../app/types';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getVerticalViewMaxWidth } from '../../../filmstrip/functions.web';
|
||||
import { getToolboxHeight } from '../../../toolbox/functions.web';
|
||||
import { isSharedVideoEnabled } from '../../functions';
|
||||
|
||||
import VideoManager from './VideoManager';
|
||||
import YoutubeVideoManager from './YoutubeVideoManager';
|
||||
@@ -33,6 +34,11 @@ interface IProps {
|
||||
*/
|
||||
filmstripWidth: number;
|
||||
|
||||
/**
|
||||
* Whether the shared video is enabled or not.
|
||||
*/
|
||||
isEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Is the video shared by the local user.
|
||||
*/
|
||||
@@ -118,7 +124,12 @@ class SharedVideo extends Component<IProps> {
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
render() {
|
||||
const { isOwner, isResizing } = this.props;
|
||||
const { isEnabled, isOwner, isResizing } = this.props;
|
||||
|
||||
if (!isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const className = !isResizing && isOwner ? '' : 'disable-pointer';
|
||||
|
||||
return (
|
||||
@@ -152,6 +163,7 @@ function _mapStateToProps(state: IReduxState) {
|
||||
clientWidth,
|
||||
filmstripVisible: visible,
|
||||
filmstripWidth: getVerticalViewMaxWidth(state),
|
||||
isEnabled: isSharedVideoEnabled(state),
|
||||
isOwner: ownerId === localParticipant?.id,
|
||||
isResizing,
|
||||
videoUrl
|
||||
|
||||
@@ -28,3 +28,13 @@ export const PLAYBACK_STATUSES = {
|
||||
PAUSED: 'pause',
|
||||
STOPPED: 'stop'
|
||||
};
|
||||
|
||||
/**
|
||||
* The domain for youtube URLs.
|
||||
*/
|
||||
export const YOUTUBE_URL_DOMAIN = 'youtube.com';
|
||||
|
||||
/**
|
||||
* The white listed domains for shared video.
|
||||
*/
|
||||
export const URL_WHITELIST = [ YOUTUBE_URL_DOMAIN ];
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { getFakeParticipants } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants';
|
||||
import {
|
||||
URL_WHITELIST,
|
||||
VIDEO_PLAYER_PARTICIPANT_NAME,
|
||||
YOUTUBE_PLAYER_PARTICIPANT_NAME,
|
||||
YOUTUBE_URL_DOMAIN
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Validates the entered video url.
|
||||
@@ -70,16 +76,22 @@ export function extractYoutubeIdOrURL(input: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const youtubeId = getYoutubeId(trimmedLink);
|
||||
if (areYoutubeURLsAllowedForSharedVideo()) {
|
||||
const youtubeId = getYoutubeId(trimmedLink);
|
||||
|
||||
if (youtubeId) {
|
||||
return youtubeId;
|
||||
if (youtubeId) {
|
||||
return youtubeId;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the URL is valid, native may crash otherwise.
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(trimmedLink);
|
||||
const url = new URL(trimmedLink);
|
||||
|
||||
if (!URL_WHITELIST.includes(url?.hostname)) {
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
@@ -87,3 +99,48 @@ export function extractYoutubeIdOrURL(input: string) {
|
||||
return trimmedLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if shared video functionality is enabled and false otherwise.
|
||||
*
|
||||
* @param {IStateful} stateful - - The redux store or {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSharedVideoEnabled(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { disableThirdPartyRequests = false } = state['features/base/config'];
|
||||
|
||||
return !disableThirdPartyRequests && URL_WHITELIST.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if you youtube URLs should be allowed for shared videos.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function areYoutubeURLsAllowedForSharedVideo() {
|
||||
return URL_WHITELIST.includes(YOUTUBE_URL_DOMAIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the passed url is allowed to be used for shared video or not.
|
||||
*
|
||||
* @param {string} url - The URL.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isURLAllowedForSharedVideo(url: string) {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
|
||||
if ([ 'http:', 'https:' ].includes(urlObject?.protocol?.toLowerCase())) {
|
||||
return URL_WHITELIST.includes(urlObject?.hostname);
|
||||
}
|
||||
} catch (_e) { // it should be youtube id.
|
||||
return areYoutubeURLsAllowedForSharedVideo();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
24
react/features/shared-video/hooks.ts
Normal file
24
react/features/shared-video/hooks.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { SharedVideoButton } from './components';
|
||||
import { isSharedVideoEnabled } from './functions';
|
||||
|
||||
const shareVideo = {
|
||||
key: 'sharedvideo',
|
||||
Content: SharedVideoButton,
|
||||
group: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the shared video button if it is enabled and undefined otherwise.
|
||||
*
|
||||
* @returns {Object | undefined}
|
||||
*/
|
||||
export function useSharedVideoButton() {
|
||||
const sharedVideoEnabled = useSelector(isSharedVideoEnabled);
|
||||
|
||||
if (sharedVideoEnabled) {
|
||||
return shareVideo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
setSharedVideoStatus
|
||||
} from './actions.any';
|
||||
import { PLAYBACK_STATUSES, SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
|
||||
import { isSharingStatus } from './functions';
|
||||
import { isSharedVideoEnabled, isSharingStatus, isURLAllowedForSharedVideo } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
|
||||
if (!isSharedVideoEnabled(state)) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOIN_IN_PROGRESS: {
|
||||
const { conference } = action;
|
||||
@@ -41,6 +45,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
({ value, attributes }: { attributes: {
|
||||
from: string; muted: string; state: string; time: string; }; value: string; }) => {
|
||||
|
||||
if (!isURLAllowedForSharedVideo(value)) {
|
||||
logger.debug(`Shared Video: Received a not allowed URL ${value}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { from } = attributes;
|
||||
const sharedVideoStatus = attributes.state;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { setDisableButton } from './actions.web';
|
||||
import { SHARED_VIDEO } from './constants';
|
||||
import { isSharedVideoEnabled } from './functions';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
@@ -13,6 +14,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOIN_IN_PROGRESS: {
|
||||
if (!isSharedVideoEnabled(state)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { conference } = action;
|
||||
|
||||
conference.addCommandListener(SHARED_VIDEO, ({ attributes }: { attributes:
|
||||
|
||||
@@ -19,6 +19,7 @@ import RecordButton from '../../../recording/components/Recording/native/RecordB
|
||||
import SecurityDialogButton
|
||||
from '../../../security/components/security-dialog/native/SecurityDialogButton';
|
||||
import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton';
|
||||
import { isSharedVideoEnabled } from '../../../shared-video/functions';
|
||||
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
|
||||
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
|
||||
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
|
||||
@@ -55,6 +56,11 @@ interface IProps {
|
||||
*/
|
||||
_isOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether the shared video is enabled or not.
|
||||
*/
|
||||
_isSharedVideoEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not speaker stats is disable.
|
||||
*/
|
||||
@@ -121,6 +127,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
|
||||
const {
|
||||
_isBreakoutRoomsSupported,
|
||||
_isSpeakerStatsDisabled,
|
||||
_isSharedVideoEnabled,
|
||||
_shouldDisplayReactionsButtons,
|
||||
_width,
|
||||
dispatch
|
||||
@@ -168,7 +175,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
|
||||
<WhiteboardButton { ...buttonProps } />
|
||||
{/* @ts-ignore */}
|
||||
<Divider style = { styles.divider as ViewStyle } />
|
||||
<SharedVideoButton { ...buttonProps } />
|
||||
{_isSharedVideoEnabled && <SharedVideoButton { ...buttonProps } />}
|
||||
{!toolbarButtons.has('screensharing') && <ScreenSharingButton { ...buttonProps } />}
|
||||
{!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
|
||||
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
|
||||
@@ -255,6 +262,7 @@ function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_customToolbarButtons: customToolbarButtons,
|
||||
_isBreakoutRoomsSupported: conference?.getBreakoutRooms()?.isSupported(),
|
||||
_isSharedVideoEnabled: isSharedVideoEnabled(state),
|
||||
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
|
||||
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state),
|
||||
_width: state['features/base/responsive-ui'].clientWidth
|
||||
|
||||
@@ -45,7 +45,7 @@ import ShareAudioButton from '../screen-share/components/web/ShareAudioButton';
|
||||
import { isScreenAudioSupported, isScreenVideoShared } from '../screen-share/functions';
|
||||
import { useSecurityDialogButton } from '../security/hooks';
|
||||
import SettingsButton from '../settings/components/web/SettingsButton';
|
||||
import SharedVideoButton from '../shared-video/components/web/SharedVideoButton';
|
||||
import { useSharedVideoButton } from '../shared-video/hooks';
|
||||
import SpeakerStats from '../speaker-stats/components/web/SpeakerStats';
|
||||
import { isSpeakerStatsDisabled } from '../speaker-stats/functions';
|
||||
import { useSpeakerStatsButton } from '../speaker-stats/hooks.web';
|
||||
@@ -142,12 +142,6 @@ const linkToSalesforce = {
|
||||
group: 2
|
||||
};
|
||||
|
||||
const shareVideo = {
|
||||
key: 'sharedvideo',
|
||||
Content: SharedVideoButton,
|
||||
group: 3
|
||||
};
|
||||
|
||||
const shareAudio = {
|
||||
key: 'shareaudio',
|
||||
Content: ShareAudioButton,
|
||||
@@ -288,6 +282,7 @@ export function useToolboxButtons(
|
||||
const liveStreaming = useLiveStreamingButton();
|
||||
const linktosalesforce = useLinkToSalesforceButton();
|
||||
const shareaudio = getShareAudioButton();
|
||||
const shareVideo = useSharedVideoButton();
|
||||
const whiteboard = useWhiteboardButton();
|
||||
const etherpad = useEtherpadButton();
|
||||
const virtualBackground = useVirtualBackgroundButton();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { playSound } from '../base/sounds/actions';
|
||||
import { showNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../recording/constants';
|
||||
import { isLiveStreamingRunning, isRecordingRunning } from '../recording/functions';
|
||||
|
||||
import { isRecorderTranscriptionsRunning } from './functions';
|
||||
|
||||
@@ -17,10 +18,10 @@ StateListenerRegistry.register(
|
||||
/* selector */ isRecorderTranscriptionsRunning,
|
||||
/* listener */ (isRecorderTranscriptionsRunningValue, { getState, dispatch }) => {
|
||||
if (isRecorderTranscriptionsRunningValue) {
|
||||
notifyTranscribingStatusChanged(true);
|
||||
notifyTranscribingStatusChanged(getState, true);
|
||||
maybeEmitRecordingNotification(dispatch, getState, true);
|
||||
} else {
|
||||
notifyTranscribingStatusChanged(false);
|
||||
notifyTranscribingStatusChanged(getState, false);
|
||||
maybeEmitRecordingNotification(dispatch, getState, false);
|
||||
}
|
||||
}
|
||||
@@ -58,11 +59,18 @@ function maybeEmitRecordingNotification(dispatch: IStore['dispatch'], getState:
|
||||
/**
|
||||
* Notify external application (if API is enabled) that transcribing has started or stopped.
|
||||
*
|
||||
* @param {Function} getState - The Redux state.
|
||||
* @param {boolean} on - True if transcribing is on, false otherwise.
|
||||
* @returns {void}
|
||||
*/
|
||||
function notifyTranscribingStatusChanged(on: boolean) {
|
||||
function notifyTranscribingStatusChanged(getState: IStore['getState'], on: boolean) {
|
||||
if (typeof APP !== 'undefined') {
|
||||
const state = getState();
|
||||
const isRecording = isRecordingRunning(state);
|
||||
const isStreaming = isLiveStreamingRunning(state);
|
||||
const mode = isRecording ? JitsiRecordingConstants.mode.FILE : JitsiRecordingConstants.mode.STREAM;
|
||||
|
||||
APP.API.notifyRecordingStatusChanged(isRecording || isStreaming, mode, undefined, on);
|
||||
APP.API.notifyTranscribingStatusChanged(on);
|
||||
}
|
||||
}
|
||||
|
||||
1
react/features/visitors/components/index.native.ts
Normal file
1
react/features/visitors/components/index.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as JoinMeetingDialog } from './native/JoinMeetingDialog';
|
||||
1
react/features/visitors/components/index.web.ts
Normal file
1
react/features/visitors/components/index.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as JoinMeetingDialog } from './web/JoinMeetingDialog';
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import Dialog from 'react-native-dialog';
|
||||
|
||||
import { StandaloneRaiseHandButton as RaiseHandButton } from '../../../reactions/components/native/RaiseHandButton';
|
||||
import styles from '../../components/native/styles';
|
||||
|
||||
/**
|
||||
* Component that renders the join meeting dialog for visitors.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function JoinMeetingDialog() {
|
||||
const { t } = useTranslation();
|
||||
const [ visible, setVisible ] = useState(true);
|
||||
|
||||
const closeDialog = useCallback(() => {
|
||||
setVisible(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog.Container
|
||||
coverScreen = { false }
|
||||
visible = { visible }>
|
||||
<Dialog.Title>{ t('visitors.joinMeeting.title') }</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{ t('visitors.joinMeeting.description') }
|
||||
<View style = { styles.raiseHandButton as ViewStyle }>
|
||||
{/* @ts-ignore */}
|
||||
<RaiseHandButton disableClick = { true } />
|
||||
</View>
|
||||
</Dialog.Description>
|
||||
<Dialog.Description>{t('visitors.joinMeeting.wishToSpeak')}</Dialog.Description>
|
||||
<Dialog.Button
|
||||
label = { t('dialog.Ok') }
|
||||
onPress = { closeDialog } />
|
||||
</Dialog.Container>
|
||||
);
|
||||
}
|
||||
12
react/features/visitors/components/native/styles.ts
Normal file
12
react/features/visitors/components/native/styles.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* The styles of the feature visitors.
|
||||
*/
|
||||
export default {
|
||||
|
||||
raiseHandButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%'
|
||||
}
|
||||
};
|
||||
75
react/features/visitors/components/web/JoinMeetingDialog.tsx
Normal file
75
react/features/visitors/components/web/JoinMeetingDialog.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { noop } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IconArrowUp } from '../../../base/icons/svg';
|
||||
import ToolboxButtonWithPopup from '../../../base/toolbox/components/web/ToolboxButtonWithPopup';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import { RaiseHandButton } from '../../../reactions/components/web/RaiseHandButton';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
raiseHand: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
pointerEvents: 'none'
|
||||
},
|
||||
raiseHandTooltip: {
|
||||
border: '1px solid #444',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2)
|
||||
},
|
||||
raiseHandButton: {
|
||||
display: 'inline-block',
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
position: 'relative'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component that renders the join meeting dialog for visitors.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function JoinMeetingDialog() {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
ok = {{ translationKey: 'dialog.Ok' }}
|
||||
titleKey = 'visitors.joinMeeting.title'>
|
||||
<div className = 'join-meeting-dialog'>
|
||||
<p>{t('visitors.joinMeeting.description')}</p>
|
||||
<div className = { classes.raiseHand }>
|
||||
<p className = { classes.raiseHandTooltip }>{t('visitors.joinMeeting.raiseHand')}</p>
|
||||
<div className = { classes.raiseHandButton }>
|
||||
<ToolboxButtonWithPopup
|
||||
icon = { IconArrowUp }
|
||||
iconDisabled = { false }
|
||||
onPopoverClose = { noop }
|
||||
onPopoverOpen = { noop }
|
||||
popoverContent = { null }
|
||||
visible = { false }>
|
||||
{/* @ts-ignore */}
|
||||
<RaiseHandButton
|
||||
disableClick = { true }
|
||||
raisedHand = { true } />
|
||||
</ToolboxButtonWithPopup>
|
||||
</div>
|
||||
</div>
|
||||
<p>{t('visitors.joinMeeting.wishToSpeak')}</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { connect, setPreferVisitor } from '../base/connection/actions';
|
||||
import { disconnect } from '../base/connection/actions.any';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { raiseHand } from '../base/participants/actions';
|
||||
import { getLocalParticipant, getParticipantById } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
updateVisitorsCount
|
||||
} from './actions';
|
||||
import { getPromotionRequests } from './functions';
|
||||
import { JoinMeetingDialog } from './components';
|
||||
import logger from './logger';
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
@@ -53,6 +55,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const { conference } = action;
|
||||
|
||||
if (getState()['features/visitors'].iAmVisitor) {
|
||||
dispatch(openDialog(JoinMeetingDialog));
|
||||
|
||||
const { demoteActorDisplayName } = getState()['features/visitors'];
|
||||
|
||||
dispatch(setVisitorDemoteActor(undefined));
|
||||
|
||||
@@ -69,10 +69,12 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action
|
||||
const existingCollabDetails = getCollabDetails(state);
|
||||
const enforceUserLimit = shouldEnforceUserLimit(state);
|
||||
const notifyUserLimit = shouldNotifyUserLimit(state);
|
||||
const iAmRecorder = Boolean(state['features/base/config'].iAmRecorder);
|
||||
|
||||
if (enforceUserLimit) {
|
||||
dispatch(restrictWhiteboard(false));
|
||||
dispatch(openDialog(WhiteboardLimitDialog));
|
||||
iAmRecorder && setTimeout(() => dispatch(hideDialog(WhiteboardLimitDialog)), 3000);
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
|
||||
server_name giphy-proxy.example.com;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/giphy-proxy.example.com.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/giphy-proxy.example.com.key;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.4.4 8.8.8.8 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
location / {
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
if ($request_method = 'POST') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
}
|
||||
if ($request_method = 'GET') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
}
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /gifs/trending {
|
||||
proxy_pass https://api.giphy.com/v1/gifs/trending;
|
||||
}
|
||||
|
||||
location /gifs/search {
|
||||
proxy_pass https://api.giphy.com/v1/gifs/search;
|
||||
}
|
||||
|
||||
location ~ /gifs/id/(.*) {
|
||||
proxy_pass https://i.giphy.com/media/$1/giphy.gif;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user