diff --git a/lang/main.json b/lang/main.json index 74aca7c5c42a5..401557058d03a 100644 --- a/lang/main.json +++ b/lang/main.json @@ -439,6 +439,8 @@ "shareScreenWarningD2": "you need to stop audio sharing, start screen sharing and check the \"share audio\" option.", "shareScreenWarningH1": "If you want to share just your screen:", "shareScreenWarningTitle": "You need to stop audio sharing before sharing your screen", + "shareVideoConfirmPlay": "You’re about to open an external website. Do you want to continue?", + "shareVideoConfirmPlayTitle": "{{name}} has shared a video with you.", "shareVideoLinkError": "Please provide a correct video link.", "shareVideoTitle": "Share video", "shareYourScreen": "Share your screen", diff --git a/modules/API/API.js b/modules/API/API.js index 835d00a90c87c..d4887e3d7a426 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -547,7 +547,7 @@ function initCommands() { }, 'start-share-video': url => { sendAnalytics(createApiEvent('share.video.start')); - const id = extractYoutubeIdOrURL(url, APP.store.getState()['features/shared-video'].allowedUrlDomains); + const id = extractYoutubeIdOrURL(url); if (id) { APP.store.dispatch(playSharedVideo(id)); diff --git a/react/features/shared-video/actionTypes.ts b/react/features/shared-video/actionTypes.ts index 1cb641ad0ccdd..e96d4d280c860 100644 --- a/react/features/shared-video/actionTypes.ts +++ b/react/features/shared-video/actionTypes.ts @@ -19,6 +19,25 @@ export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS'; */ export const RESET_SHARED_VIDEO_STATUS = 'RESET_SHARED_VIDEO_STATUS'; +/** + * The type of the action which signals to mark that dialog for asking whether to load shared video is currently shown. + * + * { + * type: SET_DIALOG_IN_PROGRESS + * } + */ +export const SET_DIALOG_IN_PROGRESS = 'SET_DIALOG_IN_PROGRESS'; + +/** + * The type of the action which signals to mark that dialog for asking whether to load shared video is shown. + * + * { + * type: SET_DIALOG_SHOWN + * } + */ +export const SET_DIALOG_SHOWN = 'SET_DIALOG_SHOWN'; + + /** * The type of the action which signals to disable or enable the shared video * button. diff --git a/react/features/shared-video/actions.any.ts b/react/features/shared-video/actions.any.ts index 8d3cd3fade957..9588d9bd1b936 100644 --- a/react/features/shared-video/actions.any.ts +++ b/react/features/shared-video/actions.any.ts @@ -3,9 +3,42 @@ import { getCurrentConference } from '../base/conference/functions'; import { openDialog } from '../base/dialog/actions'; import { getLocalParticipant } from '../base/participants/functions'; -import { RESET_SHARED_VIDEO_STATUS, SET_ALLOWED_URL_DOMAINS, SET_SHARED_VIDEO_STATUS } from './actionTypes'; +import { + RESET_SHARED_VIDEO_STATUS, + SET_ALLOWED_URL_DOMAINS, SET_DIALOG_IN_PROGRESS, + SET_DIALOG_SHOWN, + SET_SHARED_VIDEO_STATUS +} from './actionTypes'; import { SharedVideoDialog } from './components'; -import { isSharedVideoEnabled, isURLAllowedForSharedVideo } from './functions'; +import { isSharedVideoEnabled } from './functions'; + +/** + * Marks dialog is in progress. + * + * @param {boolean} value - The value to set. + * @returns {{ + * type: SET_DIALOG_IN_PROGRESS, + * }} + */ +export function setDialogInProgress(value: boolean) { + return { + type: SET_DIALOG_IN_PROGRESS, + value + }; +} + +/** + * Marks that dialog was shown. + * + * @returns {{ + * type: SET_DIALOG_SHOWN, + * }} + */ +export function setDialogShown() { + return { + type: SET_DIALOG_SHOWN + }; +} /** * Resets the status of the shared video. @@ -90,8 +123,7 @@ export function stopSharedVideo() { */ export function playSharedVideo(videoUrl: string) { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { - if (!isSharedVideoEnabled(getState()) - || !isURLAllowedForSharedVideo(videoUrl, getState()['features/shared-video'].allowedUrlDomains, true)) { + if (!isSharedVideoEnabled(getState())) { return; } const conference = getCurrentConference(getState()); diff --git a/react/features/shared-video/actions.native.ts b/react/features/shared-video/actions.native.ts index 02b37d475df7c..a05668afe278b 100644 --- a/react/features/shared-video/actions.native.ts +++ b/react/features/shared-video/actions.native.ts @@ -1 +1,35 @@ +import i18n from 'i18next'; + +import { IStore } from '../app/types'; +import { openDialog } from '../base/dialog/actions'; +import ConfirmDialog from '../base/dialog/components/native/ConfirmDialog'; + +import { setDialogInProgress } from './actions.any'; + export * from './actions.any'; + +/** + * Shows a confirmation dialog whether to play the external video link. + * + * @param {string} actor - The actor's name. + * @param {Function} onSubmit - The function to execute when confirmed. + * + * @returns {Function} + */ +export function showConfirmPlayingDialog(actor: String, onSubmit: Function): Function { + return (dispatch: IStore['dispatch']) => { + dispatch(openDialog(ConfirmDialog, { + cancelLabel: 'dialog.Cancel', + confirmLabel: 'dialog.Ok', + descriptionKey: 'dialog.shareVideoConfirmPlay', + onCancel: () => dispatch(setDialogInProgress(false)), + onSubmit: () => { + dispatch(setDialogInProgress(false)); + onSubmit(); + }, + title: i18n.t('dialog.shareVideoConfirmPlayTitle', { + name: actor + }) + })); + }; +} diff --git a/react/features/shared-video/actions.web.ts b/react/features/shared-video/actions.web.ts index 769be0e9f3028..4fd2977d42bad 100644 --- a/react/features/shared-video/actions.web.ts +++ b/react/features/shared-video/actions.web.ts @@ -1,4 +1,9 @@ +import { IStore } from '../app/types'; +import { openDialog } from '../base/dialog/actions'; + import { SET_DISABLE_BUTTON } from './actionTypes'; +import { setDialogInProgress, setDialogShown } from './actions.any'; +import ShareVideoConfirmDialog from './components/web/ShareVideoConfirmDialog'; export * from './actions.any'; @@ -17,3 +22,26 @@ export function setDisableButton(disabled: boolean) { disabled }; } + +/** + * Shows a confirmation dialog whether to play the external video link. + * + * @param {string} actor - The actor's name. + * @param {Function} onSubmit - The function to execute when confirmed. + * + * @returns {Function} + */ +export function showConfirmPlayingDialog(actor: String, onSubmit: Function) { + return (dispatch: IStore['dispatch']) => { + dispatch(setDialogInProgress(true)); + dispatch(setDialogShown()); + dispatch(openDialog(ShareVideoConfirmDialog, { + actorName: actor, + onCancel: () => dispatch(setDialogInProgress(false)), + onSubmit: () => { + dispatch(setDialogInProgress(false)); + onSubmit(); + } + })); + }; +} diff --git a/react/features/shared-video/components/AbstractSharedVideoDialog.tsx b/react/features/shared-video/components/AbstractSharedVideoDialog.tsx index bbccfbf656e91..ff4752f5615a5 100644 --- a/react/features/shared-video/components/AbstractSharedVideoDialog.tsx +++ b/react/features/shared-video/components/AbstractSharedVideoDialog.tsx @@ -53,9 +53,9 @@ export default class AbstractSharedVideoDialog extends Component < IProps, S * @returns {boolean} */ _onSetVideoLink(link: string) { - const { _allowedUrlDomains, onPostSubmit } = this.props; + const { onPostSubmit } = this.props; - const id = extractYoutubeIdOrURL(link, _allowedUrlDomains); + const id = extractYoutubeIdOrURL(link); if (!id) { return false; diff --git a/react/features/shared-video/components/web/ShareVideoConfirmDialog.tsx b/react/features/shared-video/components/web/ShareVideoConfirmDialog.tsx new file mode 100644 index 0000000000000..5475feb35f6df --- /dev/null +++ b/react/features/shared-video/components/web/ShareVideoConfirmDialog.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { DialogProps } from '../../../base/dialog/constants'; +import Dialog from '../../../base/ui/components/web/Dialog'; + +interface IProps extends DialogProps { + + /** + * The name of the remote participant that shared the video. + */ + actorName: string; + + /** + * The function to execute when canceled. + */ + onCancel: () => void; + + /** + * The function to execute when confirmed. + */ + onSubmit: () => void; +} + +/** + * Dialog to confirm playing a video shared from a remote participant. + * + * @returns {JSX.Element} + */ +export default function ShareVideoConfirmDialog({ actorName, onCancel, onSubmit }: IProps): JSX.Element { + const { t } = useTranslation(); + + return ( + +
+ { t('dialog.shareVideoConfirmPlay') } +
+
+ ); +} diff --git a/react/features/shared-video/functions.ts b/react/features/shared-video/functions.ts index 0db7138f730ab..9f07438af7fd9 100644 --- a/react/features/shared-video/functions.ts +++ b/react/features/shared-video/functions.ts @@ -63,10 +63,9 @@ export function isVideoPlaying(stateful: IStateful): boolean { * Extracts a Youtube id or URL from the user input. * * @param {string} input - The user input. - * @param {Array} allowedUrlDomains - The allowed URL domains for shared video. * @returns {string|undefined} */ -export function extractYoutubeIdOrURL(input: string, allowedUrlDomains?: Array) { +export function extractYoutubeIdOrURL(input: string) { if (!input) { return; } @@ -77,15 +76,17 @@ export function extractYoutubeIdOrURL(input: string, allowedUrlDomains?: Array 0; + return !disableThirdPartyRequests; } /** diff --git a/react/features/shared-video/middleware.any.ts b/react/features/shared-video/middleware.any.ts index 3fc81e3ae1721..3229cebe8a77d 100644 --- a/react/features/shared-video/middleware.any.ts +++ b/react/features/shared-video/middleware.any.ts @@ -8,7 +8,7 @@ import { SET_CONFIG } from '../base/config/actionTypes'; import { MEDIA_TYPE } from '../base/media/constants'; import { PARTICIPANT_LEFT } from '../base/participants/actionTypes'; import { participantJoined, participantLeft, pinParticipant } from '../base/participants/actions'; -import { getLocalParticipant, getParticipantById } from '../base/participants/functions'; +import { getLocalParticipant, getParticipantById, getParticipantDisplayName } from '../base/participants/functions'; import { FakeParticipant } from '../base/participants/types'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import { SET_DYNAMIC_BRANDING_DATA } from '../dynamic-branding/actionTypes'; @@ -17,8 +17,9 @@ import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionType import { resetSharedVideoStatus, setAllowedUrlDomians, - setSharedVideoStatus -} from './actions.any'; + setSharedVideoStatus, + showConfirmPlayingDialog +} from './actions'; import { DEFAULT_ALLOWED_URL_DOMAINS, PLAYBACK_STATUSES, @@ -53,18 +54,29 @@ MiddlewareRegistry.register(store => next => action => { from: string; muted: string; state: string; time: string; }; value: string; }) => { const state = getState(); - if (!isURLAllowedForSharedVideo(value, getState()['features/shared-video'].allowedUrlDomains, true)) { - logger.debug(`Shared Video: Received a not allowed URL ${value}`); - - return; - } - const { from } = attributes; const sharedVideoStatus = attributes.state; if (isSharingStatus(sharedVideoStatus)) { - handleSharingVideoStatus(store, value, attributes, conference); - } else if (sharedVideoStatus === 'stop') { + if (getState()['features/shared-video'].dialogInProgress) { + return; + } + + if (isURLAllowedForSharedVideo(value) || localParticipantId === from + || getState()['features/shared-video'].dialogShown) { + handleSharingVideoStatus(store, value, attributes, conference); + } else { + dispatch(showConfirmPlayingDialog(getParticipantDisplayName(getState(), from), () => { + handleSharingVideoStatus(store, value, attributes, conference); + + return true; // on mobile this is used to close the dialog + })); + } + + return; + } + + if (sharedVideoStatus === 'stop') { const videoParticipant = getParticipantById(state, value); dispatch(participantLeft(value, conference, { diff --git a/react/features/shared-video/reducer.ts b/react/features/shared-video/reducer.ts index f3bd9d9a40895..31d5a4bdd271d 100644 --- a/react/features/shared-video/reducer.ts +++ b/react/features/shared-video/reducer.ts @@ -3,17 +3,23 @@ import ReducerRegistry from '../base/redux/ReducerRegistry'; import { RESET_SHARED_VIDEO_STATUS, SET_ALLOWED_URL_DOMAINS, + SET_DIALOG_IN_PROGRESS, + SET_DIALOG_SHOWN, SET_DISABLE_BUTTON, SET_SHARED_VIDEO_STATUS } from './actionTypes'; import { DEFAULT_ALLOWED_URL_DOMAINS } from './constants'; const initialState = { - allowedUrlDomains: DEFAULT_ALLOWED_URL_DOMAINS + allowedUrlDomains: DEFAULT_ALLOWED_URL_DOMAINS, + dialogInProgress: false, + dialogShown: false }; export interface ISharedVideoState { allowedUrlDomains: Array; + dialogInProgress: boolean; + dialogShown: boolean; disabled?: boolean; muted?: boolean; ownerId?: string; @@ -36,6 +42,17 @@ ReducerRegistry.register('features/shared-video', ...initialState, allowedUrlDomains: state.allowedUrlDomains }; + case SET_DIALOG_IN_PROGRESS: { + return { + ...state, + dialogInProgress: action.value + }; + } + case SET_DIALOG_SHOWN: + return { + ...state, + dialogShown: true + }; case SET_SHARED_VIDEO_STATUS: return { ...state,