// Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { MouseEvent, ReactNode, useCallback, useEffect, useRef, useState, } from 'react'; import moment from 'moment'; import classNames from 'classnames'; import { createPortal } from 'react-dom'; import * as GoogleChrome from '../util/GoogleChrome'; import { AttachmentType, isGIF } from '../types/Attachment'; import { Avatar, AvatarSize } from './Avatar'; import { ConversationType } from '../state/ducks/conversations'; import { IMAGE_PNG, isImage, isVideo } from '../types/MIME'; import { LocalizerType } from '../types/Util'; import { MediaItemType, MessageAttributesType } from '../types/MediaItem'; export type PropsType = { children?: ReactNode; close: () => void; getConversation?: (id: string) => ConversationType; i18n: LocalizerType; media: Array; onForward?: (messageId: string) => void; onSave?: (options: { attachment: AttachmentType; message: MessageAttributesType; index: number; }) => void; selectedIndex?: number; }; export function Lightbox({ children, close, getConversation, media, i18n, onForward, onSave, selectedIndex: initialSelectedIndex, }: PropsType): JSX.Element | null { const [root, setRoot] = React.useState(); const [selectedIndex, setSelectedIndex] = useState( initialSelectedIndex || 0 ); const [previousFocus, setPreviousFocus] = useState(); const [zoomed, setZoomed] = useState(false); const containerRef = useRef(null); const focusRef = useRef(null); const videoRef = useRef(null); const restorePreviousFocus = useCallback(() => { if (previousFocus && previousFocus.focus) { previousFocus.focus(); } }, [previousFocus]); const onPrevious = useCallback(() => { setSelectedIndex(prevSelectedIndex => Math.max(prevSelectedIndex - 1, 0)); }, []); const onNext = useCallback(() => { setSelectedIndex(prevSelectedIndex => Math.min(prevSelectedIndex + 1, media.length - 1) ); }, [media]); const handleSave = () => { const mediaItem = media[selectedIndex]; const { attachment, message, index } = mediaItem; onSave?.({ attachment, message, index }); }; const handleForward = () => { close(); const mediaItem = media[selectedIndex]; onForward?.(mediaItem.message.id); }; const onKeyDown = useCallback( (event: KeyboardEvent) => { switch (event.key) { case 'Escape': if (zoomed) { setZoomed(false); } else { close(); } event.preventDefault(); event.stopPropagation(); break; case 'ArrowLeft': if (onPrevious) { onPrevious(); event.preventDefault(); event.stopPropagation(); } break; case 'ArrowRight': if (onNext) { onNext(); event.preventDefault(); event.stopPropagation(); } break; default: } }, [close, onNext, onPrevious, zoomed] ); const stopPropagationAndClose = (event: MouseEvent) => { event.stopPropagation(); close(); }; const playVideo = () => { const video = videoRef.current; if (!video) { return; } if (video.paused) { video.play(); } else { video.pause(); } }; useEffect(() => { const div = document.createElement('div'); document.body.appendChild(div); setRoot(div); return () => { document.body.removeChild(div); setRoot(undefined); }; }, []); useEffect(() => { if (!previousFocus) { setPreviousFocus(document.activeElement as HTMLElement); } }, [previousFocus]); useEffect(() => { return () => { restorePreviousFocus(); }; }, [restorePreviousFocus]); useEffect(() => { const useCapture = true; document.addEventListener('keydown', onKeyDown, useCapture); return () => { document.removeEventListener('keydown', onKeyDown, useCapture); }; }, [onKeyDown]); useEffect(() => { // Wait until we're added to the DOM. ConversationView first creates this // view, then appends its elements into the DOM. const timeout = window.setTimeout(() => { playVideo(); if (focusRef && focusRef.current) { focusRef.current.focus(); } }); return () => { if (timeout) { window.clearTimeout(timeout); } }; }, [selectedIndex]); const { attachment, contentType, loop = false, objectURL, message } = media[selectedIndex] || {}; const caption = attachment?.caption; let content: JSX.Element; if (!contentType) { content = <>{children}; } else { const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType); const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType); const isUnsupportedImageType = !isImageTypeSupported && isImage(contentType); const isUnsupportedVideoType = !isVideoTypeSupported && isVideo(contentType); if (isImageTypeSupported) { if (objectURL) { content = ( ); } else { content = ( ))} )} )} , root ) : null; } function LightboxHeader({ getConversation, i18n, message, }: { getConversation: (id: string) => ConversationType; i18n: LocalizerType; message: MessageAttributesType; }): JSX.Element { const conversation = getConversation(message.conversationId); return (
{conversation.title}
{moment(message.received_at_ms).format('L LT')}
); }