diff --git a/lang/main.json b/lang/main.json index 74aca7c5c42..401557058d0 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 835d00a90c8..d4887e3d7a4 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 1cb641ad0cc..10a981db12e 100644 --- a/react/features/shared-video/actionTypes.ts +++ b/react/features/shared-video/actionTypes.ts @@ -19,6 +19,16 @@ 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 marks that the user had confirmed to play video. + * + * { + * type: SET_CONFIRM_SHOW_VIDEO + * } + */ +export const SET_CONFIRM_SHOW_VIDEO = 'SET_CONFIRM_SHOW_VIDEO'; + + /** * 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 8d3cd3fade9..ba0ca210e23 100644 --- a/react/features/shared-video/actions.any.ts +++ b/react/features/shared-video/actions.any.ts @@ -3,9 +3,31 @@ 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 { SharedVideoDialog } from './components'; -import { isSharedVideoEnabled, isURLAllowedForSharedVideo } from './functions'; +import { + RESET_SHARED_VIDEO_STATUS, + SET_ALLOWED_URL_DOMAINS, + SET_CONFIRM_SHOW_VIDEO, + SET_SHARED_VIDEO_STATUS +} from './actionTypes'; +import { ShareVideoConfirmDialog, SharedVideoDialog } from './components'; +import { PLAYBACK_START, PLAYBACK_STATUSES } from './constants'; +import { isSharedVideoEnabled } from './functions'; + + +/** + * Marks that user confirmed or not to play video. + * + * @param {boolean} value - The value to set. + * @returns {{ + * type: SET_CONFIRM_SHOW_VIDEO, + * }} + */ +export function setConfirmShowVideo(value: boolean) { + return { + type: SET_CONFIRM_SHOW_VIDEO, + value + }; +} /** * Resets the status of the shared video. @@ -90,8 +112,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()); @@ -101,7 +122,7 @@ export function playSharedVideo(videoUrl: string) { dispatch(setSharedVideoStatus({ videoUrl, - status: 'start', + status: PLAYBACK_START, time: 0, ownerId: localParticipant?.id })); @@ -120,7 +141,7 @@ export function toggleSharedVideo() { const state = getState(); const { status = '' } = state['features/shared-video']; - if ([ 'playing', 'start', 'pause' ].includes(status)) { + if ([ PLAYBACK_STATUSES.PLAYING, PLAYBACK_START, PLAYBACK_STATUSES.PAUSED ].includes(status)) { dispatch(stopSharedVideo()); } else { dispatch(showSharedVideoDialog((id: string) => dispatch(playSharedVideo(id)))); @@ -143,3 +164,26 @@ export function setAllowedUrlDomians(allowedUrlDomains: Array) { allowedUrlDomains }; } + +/** + * 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']) => { + // shows only one dialog at a time + dispatch(setConfirmShowVideo(false)); + + dispatch(openDialog(ShareVideoConfirmDialog, { + actorName: actor, + onSubmit: () => { + dispatch(setConfirmShowVideo(true)); + onSubmit(); + } + })); + }; +} diff --git a/react/features/shared-video/components/AbstractSharedVideoDialog.tsx b/react/features/shared-video/components/AbstractSharedVideoDialog.tsx index bbccfbf656e..ff4752f5615 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/index.native.ts b/react/features/shared-video/components/index.native.ts index 699685be5d6..14eefdd4421 100644 --- a/react/features/shared-video/components/index.native.ts +++ b/react/features/shared-video/components/index.native.ts @@ -1,3 +1,4 @@ // @ts-ignore export { default as SharedVideoDialog } from './native/SharedVideoDialog'; export { default as SharedVideoButton } from './native/SharedVideoButton'; +export { default as ShareVideoConfirmDialog } from './native/ShareVideoConfirmDialog'; diff --git a/react/features/shared-video/components/index.web.ts b/react/features/shared-video/components/index.web.ts index 617f11369fa..af18c8ecb09 100644 --- a/react/features/shared-video/components/index.web.ts +++ b/react/features/shared-video/components/index.web.ts @@ -1,2 +1,3 @@ export { default as SharedVideoDialog } from './web/SharedVideoDialog'; export { default as SharedVideoButton } from './web/SharedVideoButton'; +export { default as ShareVideoConfirmDialog } from './web/ShareVideoConfirmDialog'; diff --git a/react/features/shared-video/components/native/ShareVideoConfirmDialog.tsx b/react/features/shared-video/components/native/ShareVideoConfirmDialog.tsx new file mode 100644 index 00000000000..68afb5b5453 --- /dev/null +++ b/react/features/shared-video/components/native/ShareVideoConfirmDialog.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog'; +import { DialogProps } from '../../../base/dialog/constants'; + +interface IProps extends DialogProps { + + /** + * The name of the remote participant that shared the video. + */ + actorName: string; + + /** + * 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, onSubmit }: IProps): JSX.Element { + const { t } = useTranslation(); + + return ( + + ); +} diff --git a/react/features/shared-video/components/native/YoutubeVideoManager.tsx b/react/features/shared-video/components/native/YoutubeVideoManager.tsx index e4b9a78e70e..bdefe3d0a7a 100644 --- a/react/features/shared-video/components/native/YoutubeVideoManager.tsx +++ b/react/features/shared-video/components/native/YoutubeVideoManager.tsx @@ -130,7 +130,7 @@ class YoutubeVideoManager extends AbstractVideoManager { }); } - if (event === 'playing') { + if (event === PLAYBACK_STATUSES.PLAYING) { this.setState({ paused: 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 00000000000..c479d208572 --- /dev/null +++ b/react/features/shared-video/components/web/ShareVideoConfirmDialog.tsx @@ -0,0 +1,39 @@ +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 confirmed. + */ + onSubmit: () => void; +} + +/** + * Dialog to confirm playing a video shared from a remote participant. + * + * @returns {JSX.Element} + */ +export default function ShareVideoConfirmDialog({ actorName, onSubmit }: IProps): JSX.Element { + const { t } = useTranslation(); + + return ( + +
+ { t('dialog.shareVideoConfirmPlay') } +
+
+ ); +} diff --git a/react/features/shared-video/constants.ts b/react/features/shared-video/constants.ts index 2305a0848f6..1a07a2cb76b 100644 --- a/react/features/shared-video/constants.ts +++ b/react/features/shared-video/constants.ts @@ -29,6 +29,11 @@ export const PLAYBACK_STATUSES = { STOPPED: 'stop' }; +/** + * Playback start state. + */ +export const PLAYBACK_START = 'start'; + /** * The domain for youtube URLs. */ diff --git a/react/features/shared-video/functions.ts b/react/features/shared-video/functions.ts index 0db7138f730..f29d47f1a8f 100644 --- a/react/features/shared-video/functions.ts +++ b/react/features/shared-video/functions.ts @@ -4,6 +4,8 @@ import { toState } from '../base/redux/functions'; import { ALLOW_ALL_URL_DOMAINS, + PLAYBACK_START, + PLAYBACK_STATUSES, VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME, YOUTUBE_URL_DOMAIN @@ -35,7 +37,7 @@ function getYoutubeId(url: string) { * @returns {boolean} */ export function isSharingStatus(status: string) { - return [ 'playing', 'pause', 'start' ].includes(status); + return [ PLAYBACK_STATUSES.PLAYING, PLAYBACK_STATUSES.PAUSED, PLAYBACK_START ].includes(status); } @@ -63,10 +65,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 +78,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 3fc81e3ae17..328f8c8c120 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,10 +17,12 @@ import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionType import { resetSharedVideoStatus, setAllowedUrlDomians, - setSharedVideoStatus + setSharedVideoStatus, + showConfirmPlayingDialog } from './actions.any'; import { DEFAULT_ALLOWED_URL_DOMAINS, + PLAYBACK_START, PLAYBACK_STATUSES, SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME @@ -53,18 +55,33 @@ 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') { + // confirmShowVideo is undefined the first time we receive + // when confirmShowVideo is false we ignore everything except stop that resets it + if (getState()['features/shared-video'].confirmShowVideo === false) { + return; + } + + if (isURLAllowedForSharedVideo(value) + || localParticipantId === from + || getState()['features/shared-video'].confirmShowVideo) { // if confirmed skip asking again + 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, { @@ -189,8 +206,16 @@ function handleSharingVideoStatus(store: IStore, videoUrl: string, const { dispatch, getState } = store; const localParticipantId = getLocalParticipant(getState())?.id; const oldStatus = getState()['features/shared-video']?.status ?? ''; + const oldVideoUrl = getState()['features/shared-video'].videoUrl; + + if (oldVideoUrl && oldVideoUrl !== videoUrl) { + logger.warn( + `User with id: ${localParticipantId} sent videoUrl: ${videoUrl} while we are playing: ${oldVideoUrl}`); + + return; + } - if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) { + if (state === PLAYBACK_START && !isSharingStatus(oldStatus)) { const youtubeId = videoUrl.match(/http/) ? false : videoUrl; const avatarURL = youtubeId ? `https://img.youtube.com/vi/${youtubeId}/0.jpg` : ''; diff --git a/react/features/shared-video/middleware.web.ts b/react/features/shared-video/middleware.web.ts index f9f573f9e4d..5c3eb360504 100644 --- a/react/features/shared-video/middleware.web.ts +++ b/react/features/shared-video/middleware.web.ts @@ -3,7 +3,7 @@ import { getLocalParticipant } from '../base/participants/functions'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import { setDisableButton } from './actions.web'; -import { SHARED_VIDEO } from './constants'; +import { PLAYBACK_STATUSES, SHARED_VIDEO } from './constants'; import { isSharedVideoEnabled } from './functions'; import './middleware.any'; @@ -25,7 +25,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { const { from } = attributes; const status = attributes.state; - if (status === 'playing') { + if (status === PLAYBACK_STATUSES.PLAYING) { if (localParticipantId !== from) { dispatch(setDisableButton(true)); } diff --git a/react/features/shared-video/reducer.ts b/react/features/shared-video/reducer.ts index f3bd9d9a408..c3e98865f9a 100644 --- a/react/features/shared-video/reducer.ts +++ b/react/features/shared-video/reducer.ts @@ -3,6 +3,7 @@ import ReducerRegistry from '../base/redux/ReducerRegistry'; import { RESET_SHARED_VIDEO_STATUS, SET_ALLOWED_URL_DOMAINS, + SET_CONFIRM_SHOW_VIDEO, SET_DISABLE_BUTTON, SET_SHARED_VIDEO_STATUS } from './actionTypes'; @@ -14,6 +15,7 @@ const initialState = { export interface ISharedVideoState { allowedUrlDomains: Array; + confirmShowVideo?: boolean; disabled?: boolean; muted?: boolean; ownerId?: string; @@ -36,6 +38,12 @@ ReducerRegistry.register('features/shared-video', ...initialState, allowedUrlDomains: state.allowedUrlDomains }; + case SET_CONFIRM_SHOW_VIDEO: { + return { + ...state, + confirmShowVideo: action.value + }; + } case SET_SHARED_VIDEO_STATUS: return { ...state,