Compare commits

...

13 Commits

Author SHA1 Message Date
Hristo Terezov
13d3ec9f6d fix(conference): Make sure join waits for confernce.init.
It was possible that join can be executed before conference.init have even started or we haven't reached the point ot create the initialGUMPromise. This was causing the following issues:
 - users stuck on the prejoin screen
 - participants join 2+ times in the call (we have been creating more than 1 local participants from a single page).
2024-10-25 15:33:02 -05:00
Hristo Terezov
f7acc2e15f feat(shared-video): Allow only whitelisted URLs. 2024-08-13 12:45:42 -05:00
Hristo Terezov
a2c09357ca fix(gif): Restrict gif rendering to Giphy only 2024-08-09 10:25:47 -05:00
Hristo Terezov
3ed68fdf1a fix(giphy): Remove proxyUrl config option. 2024-08-09 10:20:49 -05:00
Mihaela Dumitru
df010e6a1d fix(whiteboard) remove limit dialog for jibri (#14947) (#14952) 2024-07-30 16:49:16 +03:00
Hristo Terezov
c901c18e21 fix(push2talk): incorect state on release because a new audio track is beening created. (part 2) 2024-07-23 18:19:36 -05:00
Hristo Terezov
d57b48f8d7 fix(push2talk): incorect state on release because a new audio track is beening created.
We are ending up in incorrect mute state (unmuted) if the initial press event is resulting in a new track creation and the release event happens before the track is created.
2024-07-23 10:13:52 -05:00
Saúl Ibarra Corretgé
c549c4ce5b fix(keyboard-shortcuts) fix PTT on keyboards which send repeated keys
Come over for a fun story, dear reader!

Here is a not-so-fun difference in behavior, observed in macOS:

- The builtin keyboard doesn't seem to send the same key over and over
  again while it's being held.
- On the contrary, a USB keyboard does.

That means that for some keyboards PTT has been broken. We get
keydown/keyup pairs in quick successing.

One would think that KeyboardEvent.repeat would solve that, but it
doesn't seem to, in practice. See: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat

So, in order to solve this, delay handling the keyup event by 50ms. This
way, if a new keydown comes before the keyup has been handled we'll
cancel it and act as it never happened, restoring PTT functionality.

While we're at it, use window.addEventListener rather than
onkeyup/onkeydown, since it's 2024 :-)
2024-07-23 10:13:33 -05:00
Mihaela Dumitru
0951a9dd9a fix(visitors) update dialog button (#14936) 2024-07-22 16:31:19 +03:00
Mihaela Dumitru
133e36ead6 feat(visitors) add info dialog (#14926) 2024-07-19 10:17:10 +03:00
Saúl Ibarra Corretgé
914ad85baa feat(external_api) add ability to start transcriptions together with recordings 2024-07-03 20:52:38 +02:00
Saúl Ibarra Corretgé
70c3a2b01c feat(external_api) include transcription state in recordingStatusChanged 2024-07-03 20:52:38 +02:00
Saúl Ibarra Corretgé
0ce193e5c3 feat(recording) add isLiveStreamingRunning helper 2024-07-03 20:52:38 +02:00
44 changed files with 589 additions and 212 deletions

View File

@@ -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();
})

View File

@@ -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

View File

@@ -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",

View File

@@ -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
});
}

View File

@@ -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);
}
/**

View File

@@ -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;

View File

@@ -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
};
}

View File

@@ -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);

View File

@@ -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 ]);

View File

@@ -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
};
}

View File

@@ -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);

View File

@@ -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.
*/

View File

@@ -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 ]);

View File

@@ -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());

View File

@@ -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();
}, []);

View File

@@ -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`;
}

View File

@@ -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();
}
};
});
};

View File

@@ -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 () => {

View File

@@ -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));

View File

@@ -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));

View File

@@ -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

View File

@@ -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
};

View File

@@ -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';
}

View File

@@ -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()));
}
}

View File

@@ -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) {

View File

@@ -1,2 +1,3 @@
// @ts-ignore
export { default as SharedVideoDialog } from './native/SharedVideoDialog';
export { default as SharedVideoButton } from './native/SharedVideoButton';

View File

@@ -1 +1,2 @@
export { default as SharedVideoDialog } from './web/SharedVideoDialog';
export { default as SharedVideoButton } from './web/SharedVideoButton';

View File

@@ -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

View File

@@ -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 ];

View File

@@ -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;
}

View 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;
}
}

View File

@@ -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;

View File

@@ -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:

View File

@@ -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

View File

@@ -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();

View File

@@ -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);
}
}

View File

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

View File

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

View File

@@ -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>
);
}

View File

@@ -0,0 +1,12 @@
/**
* The styles of the feature visitors.
*/
export default {
raiseHandButton: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%'
}
};

View 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>
);
}

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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;
}
}