From a55b410e6a8e47bc359e11d4e518cc836f3cf5d1 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 5 Aug 2022 19:22:43 +0200 Subject: [PATCH] Media Viewer: Support switching avatars (#1929) --- src/components/common/GroupChatInfo.tsx | 9 +- src/components/common/PrivateChatInfo.tsx | 9 +- src/components/common/ProfileInfo.tsx | 32 ++- src/components/common/ProfilePhoto.tsx | 3 +- src/components/left/search/MediaResults.tsx | 4 +- src/components/mediaViewer/MediaViewer.scss | 6 + src/components/mediaViewer/MediaViewer.tsx | 247 ++++++------------ .../mediaViewer/MediaViewerContent.tsx | 182 ++++--------- .../mediaViewer/MediaViewerFooter.tsx | 4 +- .../mediaViewer/MediaViewerSlides.tsx | 110 ++++---- src/components/mediaViewer/VideoPlayer.scss | 1 + src/components/mediaViewer/VideoPlayer.tsx | 15 +- .../mediaViewer/VideoPlayerControls.scss | 17 +- .../mediaViewer/VideoPlayerControls.tsx | 2 +- .../mediaViewer/hooks/useMediaProps.ts | 155 +++++++++++ .../middle/message/hooks/useInnerHandlers.ts | 7 +- src/components/right/Profile.tsx | 4 +- src/global/actions/ui/mediaViewer.ts | 4 +- src/global/selectors/ui.ts | 2 +- src/global/types.ts | 4 +- src/styles/_variables.scss | 1 + src/styles/index.scss | 2 + 22 files changed, 439 insertions(+), 381 deletions(-) create mode 100644 src/components/mediaViewer/hooks/useMediaProps.ts diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index 88f195ab0..a32698f04 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -66,22 +66,25 @@ const GroupChatInfo: FC = ({ const { loadFullChat, openMediaViewer, + loadProfilePhotos, } = getActions(); const isSuperGroup = chat && isChatSuperGroup(chat); const { id: chatId, isMin, isRestricted } = chat || {}; useEffect(() => { - if (chatId && !isMin && withFullInfo && lastSyncTime) { - loadFullChat({ chatId }); + if (chatId && !isMin && lastSyncTime) { + if (withFullInfo) loadFullChat({ chatId }); + if (withMediaViewer) loadProfilePhotos({ profileId: chatId }); } - }, [chatId, isMin, lastSyncTime, withFullInfo, loadFullChat, isSuperGroup]); + }, [chatId, isMin, lastSyncTime, withFullInfo, loadFullChat, loadProfilePhotos, isSuperGroup, withMediaViewer]); const handleAvatarViewerOpen = useCallback((e: ReactMouseEvent, hasMedia: boolean) => { if (chat && hasMedia) { e.stopPropagation(); openMediaViewer({ avatarOwnerId: chat.id, + mediaId: 0, origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar, }); } diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 0ed37f0e9..03ffdd096 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -67,22 +67,25 @@ const PrivateChatInfo: FC = ({ const { loadFullUser, openMediaViewer, + loadProfilePhotos, } = getActions(); const { id: userId } = user || {}; const fullName = getUserFullName(user); useEffect(() => { - if (withFullInfo && lastSyncTime && userId) { - loadFullUser({ userId }); + if (userId && lastSyncTime) { + if (withFullInfo) loadFullUser({ userId }); + if (withMediaViewer) loadProfilePhotos({ profileId: userId }); } - }, [userId, loadFullUser, lastSyncTime, withFullInfo]); + }, [userId, loadFullUser, loadProfilePhotos, lastSyncTime, withFullInfo, withMediaViewer]); const handleAvatarViewerOpen = useCallback((e: ReactMouseEvent, hasMedia: boolean) => { if (user && hasMedia) { e.stopPropagation(); openMediaViewer({ avatarOwnerId: user.id, + mediaId: 0, origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar, }); } diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 13b0766ec..9a11e6828 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -9,6 +9,7 @@ import type { GlobalState } from '../../global/types'; import { MediaViewerOrigin } from '../../types'; import { IS_TOUCH_ENV } from '../../util/environment'; +import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { selectChat, selectUser, selectUserStatus } from '../../global/selectors'; import { getUserFullName, getUserStatus, isChatChannel, isUserOnline, @@ -18,6 +19,7 @@ import { captureEvents, SwipeDirection } from '../../util/captureEvents'; import buildClassName from '../../util/buildClassName'; import usePhotosPreload from './hooks/usePhotosPreload'; import useLang from '../../hooks/useLang'; +import usePrevious from '../../hooks/usePrevious'; import VerifiedIcon from './VerifiedIcon'; import ProfilePhoto from './ProfilePhoto'; @@ -40,6 +42,8 @@ type StateProps = isSavedMessages?: boolean; animationLevel: 0 | 1 | 2; serverTimeOffset: number; + mediaId?: number; + avatarOwnerId?: string; } & Pick; @@ -52,6 +56,8 @@ const ProfileInfo: FC = ({ connectionState, animationLevel, serverTimeOffset, + mediaId, + avatarOwnerId, }) => { const { loadFullUser, @@ -64,15 +70,26 @@ const ProfileInfo: FC = ({ const { id: userId } = user || {}; const { id: chatId } = chat || {}; const fullName = user ? getUserFullName(user) : (chat ? chat.title : ''); - const photos = user?.photos || chat?.photos || []; - const slideAnimation = animationLevel >= 1 - ? (lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized') + const photos = user?.photos || chat?.photos || MEMO_EMPTY_ARRAY; + const prevMediaId = usePrevious(mediaId); + const prevAvatarOwnerId = usePrevious(avatarOwnerId); + const [hasSlideAnimation, setHasSlideAnimation] = useState(true); + const slideAnimation = hasSlideAnimation + ? animationLevel >= 1 ? (lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized') : 'none' : 'none'; const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0; const isLast = isSavedMessages || photos.length <= 1 || currentPhotoIndex === photos.length - 1; + // Set the current avatar photo to the last selected photo in Media Viewer after it is closed + useEffect(() => { + if (prevAvatarOwnerId && prevMediaId !== undefined && mediaId === undefined) { + setHasSlideAnimation(false); + setCurrentPhotoIndex(prevMediaId); + } + }, [mediaId, prevMediaId, prevAvatarOwnerId]); + // Deleting the last profile photo may result in an error useEffect(() => { if (currentPhotoIndex > photos.length) { @@ -91,7 +108,7 @@ const ProfileInfo: FC = ({ const handleProfilePhotoClick = useCallback(() => { openMediaViewer({ avatarOwnerId: userId || chatId, - profilePhotoIndex: currentPhotoIndex, + mediaId: currentPhotoIndex, origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar, }); }, [openMediaViewer, userId, chatId, currentPhotoIndex, forceShowSelf]); @@ -106,7 +123,7 @@ const ProfileInfo: FC = ({ if (isFirst) { return; } - + setHasSlideAnimation(true); setCurrentPhotoIndex(currentPhotoIndex - 1); }, [currentPhotoIndex, isFirst]); @@ -114,7 +131,7 @@ const ProfileInfo: FC = ({ if (isLast) { return; } - + setHasSlideAnimation(true); setCurrentPhotoIndex(currentPhotoIndex + 1); }, [currentPhotoIndex, isLast]); @@ -251,6 +268,7 @@ export default memo(withGlobal( const chat = selectChat(global, userId); const isSavedMessages = !forceShowSelf && user && user.isSelf; const { animationLevel } = global.settings.byKey; + const { mediaId, avatarOwnerId } = global.mediaViewer; return { connectionState, @@ -260,6 +278,8 @@ export default memo(withGlobal( isSavedMessages, animationLevel, serverTimeOffset, + mediaId, + avatarOwnerId, }; }, )(ProfileInfo)); diff --git a/src/components/common/ProfilePhoto.tsx b/src/components/common/ProfilePhoto.tsx index 74124a127..094288f18 100644 --- a/src/components/common/ProfilePhoto.tsx +++ b/src/components/common/ProfilePhoto.tsx @@ -15,6 +15,7 @@ import { } from '../../global/helpers'; import renderText from './helpers/renderText'; import buildClassName from '../../util/buildClassName'; +import safePlay from '../../util/safePlay'; import { getFirstLetters } from '../../util/textFormat'; import useMedia from '../../hooks/useMedia'; import useLang from '../../hooks/useLang'; @@ -80,7 +81,7 @@ const ProfilePhoto: FC = ({ videoRef.current.pause(); videoRef.current.currentTime = 0; } else { - videoRef.current.play(); + safePlay(videoRef.current); } }, [notActive]); diff --git a/src/components/left/search/MediaResults.tsx b/src/components/left/search/MediaResults.tsx index 6510d8246..0f861f584 100644 --- a/src/components/left/search/MediaResults.tsx +++ b/src/components/left/search/MediaResults.tsx @@ -79,10 +79,10 @@ const MediaResults: FC = ({ }).filter(Boolean); }, [globalMessagesByChatId, foundIds]); - const handleSelectMedia = useCallback((messageId: number, chatId: string) => { + const handleSelectMedia = useCallback((id: number, chatId: string) => { openMediaViewer({ chatId, - messageId, + mediaId: id, origin: MediaViewerOrigin.SearchResult, }); }, [openMediaViewer]); diff --git a/src/components/mediaViewer/MediaViewer.scss b/src/components/mediaViewer/MediaViewer.scss index 3ca6011be..8e9418890 100644 --- a/src/components/mediaViewer/MediaViewer.scss +++ b/src/components/mediaViewer/MediaViewer.scss @@ -176,6 +176,12 @@ } } } + + .is-protected { + user-select: none; + -webkit-touch-callout: none; + pointer-events: none; + } } .ghost { diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 170d20049..aa1f23b74 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -4,28 +4,13 @@ import React, { } from '../../lib/teact/teact'; import type { - ApiChat, ApiDimensions, ApiMessage, ApiUser, + ApiChat, ApiMessage, ApiUser, } from '../../api/types'; -import { ApiMediaFormat } from '../../api/types'; import { MediaViewerOrigin } from '../../types'; import { getActions, withGlobal } from '../../global'; import { - getChatAvatarHash, getChatMediaMessageIds, - getMessageDocument, - getMessageFileName, - getMessageMediaFormat, - getMessageMediaHash, - getMessageMediaThumbDataUri, - getMessagePhoto, - getMessageVideo, - getMessageWebPagePhoto, - getMessageWebPageVideo, - getPhotoFullDimensions, getVideoAvatarMediaHash, - getVideoDimensions, - isMessageDocumentPhoto, - isMessageDocumentVideo, } from '../../global/helpers'; import { selectChat, @@ -43,22 +28,18 @@ import { stopCurrentAudio } from '../../util/audioPlayer'; import captureEscKeyListener from '../../util/captureEscKeyListener'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import { ANIMATION_END_DELAY } from '../../config'; -import { - AVATAR_FULL_DIMENSIONS, MEDIA_VIEWER_MEDIA_QUERY, VIDEO_AVATAR_FULL_DIMENSIONS, -} from '../common/helpers/mediaDimensions'; +import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions'; import windowSize from '../../util/windowSize'; import { animateClosing, animateOpening } from './helpers/ghostAnimation'; import { renderMessageText } from '../common/helpers/renderMessageText'; -import useBlurSync from '../../hooks/useBlurSync'; import useFlag from '../../hooks/useFlag'; import useForceUpdate from '../../hooks/useForceUpdate'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; -import useMedia from '../../hooks/useMedia'; -import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress'; import usePrevious from '../../hooks/usePrevious'; +import { useMediaProps } from './hooks/useMediaProps'; import ReportModal from '../common/ReportModal'; import Button from '../ui/Button'; @@ -73,12 +54,11 @@ import './MediaViewer.scss'; type StateProps = { chatId?: string; threadId?: number; - messageId?: number; + mediaId?: number; senderId?: string; isChatWithSelf?: boolean; origin?: MediaViewerOrigin; avatarOwner?: ApiChat | ApiUser; - profilePhotoIndex?: number; message?: ApiMessage; chatMessages?: Record; collectionIds?: number[]; @@ -90,12 +70,11 @@ const ANIMATION_DURATION = 350; const MediaViewer: FC = ({ chatId, threadId, - messageId, + mediaId, senderId, isChatWithSelf, origin, avatarOwner, - profilePhotoIndex, message, chatMessages, collectionIds, @@ -109,40 +88,11 @@ const MediaViewer: FC = ({ toggleChatInfo, } = getActions(); - const isOpen = Boolean(avatarOwner || messageId); - - const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia; - const isFromSearch = origin === MediaViewerOrigin.SearchResult; - - /* Content */ - const photo = message ? getMessagePhoto(message) : undefined; - const video = message ? getMessageVideo(message) : undefined; - const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined; - const webPageVideo = message ? getMessageWebPageVideo(message) : undefined; - const isDocumentPhoto = message ? isMessageDocumentPhoto(message) : false; - const isDocumentVideo = message ? isMessageDocumentVideo(message) : false; - const isVideo = Boolean(video || webPageVideo || isDocumentVideo); - const { isGif } = video || webPageVideo || {}; - const isPhoto = Boolean(!isVideo && (photo || webPagePhoto || isDocumentPhoto)); - const isAvatar = Boolean(avatarOwner); - - /* Navigation */ - const singleMessageId = webPagePhoto || webPageVideo ? messageId : undefined; - - const messageIds = useMemo(() => { - return singleMessageId - ? [singleMessageId] - : getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia); - }, [singleMessageId, chatMessages, collectionIds, isFromSharedMedia]); - - const selectedMediaMessageIndex = messageId ? messageIds.indexOf(messageId) : -1; + const isOpen = Boolean(avatarOwner || mediaId); /* Animation */ const animationKey = useRef(); const prevSenderId = usePrevious(senderId); - if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) { - animationKey.current = selectedMediaMessageIndex; - } const headerAnimation = animationLevel === 2 ? 'slide-fade' : 'none'; const isGhostAnimation = animationLevel === 2; @@ -150,80 +100,46 @@ const MediaViewer: FC = ({ const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(); const [zoomLevelChange, setZoomLevelChange] = useState(1); - /* Media data */ - function getMediaHash(isFull?: boolean) { - if (isAvatar && profilePhotoIndex !== undefined) { - const { photos } = avatarOwner!; - const avatarPhoto = photos && photos[profilePhotoIndex]; - return avatarPhoto - // Video for avatar should be used only for full size - ? (avatarPhoto.isVideo && isFull ? getVideoAvatarMediaHash(avatarPhoto) : `photo${avatarPhoto.id}?size=c`) - : getChatAvatarHash(avatarOwner!, isFull ? 'big' : 'normal'); + const { + webPagePhoto, + webPageVideo, + isVideo, + isPhoto, + bestImageData, + dimensions, + isGif, + isFromSharedMedia, + avatarPhoto, + fileName, + fullMediaBlobUrl, + previewBlobUrl, + } = useMediaProps({ + message, avatarOwner, mediaId, delay: isGhostAnimation && ANIMATION_DURATION, + }); + + const canReport = !!avatarPhoto && !isChatWithSelf; + + /* Navigation */ + const singleMediaId = webPagePhoto || webPageVideo ? mediaId : undefined; + + const mediaIds = useMemo(() => { + if (singleMediaId) { + return [singleMediaId]; + } else if (avatarOwner) { + return avatarOwner.photos?.map((p, i) => i) || []; + } else { + return getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia); } + }, [singleMediaId, avatarOwner, chatMessages, collectionIds, isFromSharedMedia]); - return message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview'); - } + const selectedMediaIndex = mediaId ? mediaIds.indexOf(mediaId) : -1; - const pictogramBlobUrl = useMedia( - message && (isFromSharedMedia || isFromSearch) && getMessageMediaHash(message, 'pictogram'), - undefined, - ApiMediaFormat.BlobUrl, - undefined, - isGhostAnimation && ANIMATION_DURATION, - ); - const previewMediaHash = getMediaHash(); - const previewBlobUrl = useMedia( - previewMediaHash, - undefined, - ApiMediaFormat.BlobUrl, - undefined, - isGhostAnimation && ANIMATION_DURATION, - ); - const { mediaData: fullMediaBlobUrl } = useMediaWithLoadProgress( - getMediaHash(true), - undefined, - message && getMessageMediaFormat(message, 'viewerFull'), - undefined, - isGhostAnimation && ANIMATION_DURATION, - ); - const avatarPhoto = avatarOwner?.photos?.[profilePhotoIndex!]; - const isVideoAvatar = Boolean(isAvatar && avatarPhoto?.isVideo); - const canReport = !!avatarPhoto && profilePhotoIndex! > 0 && !isChatWithSelf; - - const localBlobUrl = (photo || video) ? (photo || video)!.blobUrl : undefined; - let bestImageData = (!isVideo && (localBlobUrl || fullMediaBlobUrl)) || previewBlobUrl || pictogramBlobUrl; - const thumbDataUri = useBlurSync(!bestImageData && message && getMessageMediaThumbDataUri(message)); - if (!bestImageData && origin !== MediaViewerOrigin.SearchResult) { - bestImageData = thumbDataUri; - } - if (isVideoAvatar && previewBlobUrl) { - bestImageData = previewBlobUrl; - } - - const fileName = message - ? getMessageFileName(message) - : isAvatar - ? `avatar${avatarOwner!.id}-${profilePhotoIndex}.${avatarOwner?.hasVideoAvatar ? 'mp4' : 'jpg'}` - : undefined; - - let dimensions!: ApiDimensions; - if (message) { - if (isDocumentPhoto || isDocumentVideo) { - dimensions = getMessageDocument(message)!.mediaSize!; - } else if (photo || webPagePhoto) { - dimensions = getPhotoFullDimensions((photo || webPagePhoto)!)!; - } else if (video || webPageVideo) { - dimensions = getVideoDimensions((video || webPageVideo)!)!; - } - } else { - dimensions = isVideoAvatar ? VIDEO_AVATAR_FULL_DIMENSIONS : AVATAR_FULL_DIMENSIONS; + if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) { + animationKey.current = selectedMediaIndex; } useEffect(() => { - if (!IS_SINGLE_COLUMN_LAYOUT) { - return; - } - + if (!IS_SINGLE_COLUMN_LAYOUT) return; document.body.classList.toggle('is-media-viewer-open', isOpen); }, [isOpen]); @@ -277,28 +193,31 @@ const MediaViewer: FC = ({ if (IS_SINGLE_COLUMN_LAYOUT) { setTimeout(() => { toggleChatInfo(false, { forceSyncOnIOs: true }); - focusMessage({ chatId, threadId, messageId }); + focusMessage({ chatId, threadId, mediaId }); }, ANIMATION_DURATION); } else { - focusMessage({ chatId, threadId, messageId }); + focusMessage({ chatId, threadId, mediaId }); } - }, [close, chatId, threadId, focusMessage, toggleChatInfo, messageId]); + }, [close, chatId, threadId, focusMessage, toggleChatInfo, mediaId]); const handleForward = useCallback(() => { openForwardMenu({ fromChatId: chatId, - messageIds: [messageId], + messageIds: [mediaId], }); - }, [openForwardMenu, chatId, messageId]); + }, [openForwardMenu, chatId, mediaId]); - const selectMessage = useCallback((id?: number) => openMediaViewer({ - chatId, - threadId, - messageId: id, - origin, - }, { - forceOnHeavyAnimation: true, - }), [chatId, openMediaViewer, origin, threadId]); + const selectMedia = useCallback((id?: number) => { + openMediaViewer({ + chatId, + threadId, + mediaId: id, + avatarOwnerId: avatarOwner?.id, + origin, + }, { + forceOnHeavyAnimation: true, + }); + }, [avatarOwner?.id, chatId, openMediaViewer, origin, threadId]); useEffect(() => (isOpen ? captureEscKeyListener(() => { close(); @@ -323,14 +242,14 @@ const MediaViewer: FC = ({ }; }, [isOpen]); - const getMessageId = useCallback((fromId?: number, direction?: number): number | undefined => { - if (!fromId) return undefined; - const index = messageIds.indexOf(fromId); - if ((direction === -1 && index > 0) || (direction === 1 && index < messageIds.length - 1)) { - return messageIds[index + direction]; + const getMediaId = useCallback((fromId?: number, direction?: number): number | undefined => { + if (fromId === undefined) return undefined; + const index = mediaIds.indexOf(fromId); + if ((direction === -1 && index > 0) || (direction === 1 && index < mediaIds.length - 1)) { + return mediaIds[index + direction]; } return undefined; - }, [messageIds]); + }, [mediaIds]); const lang = useLang(); @@ -340,17 +259,17 @@ const MediaViewer: FC = ({ }); function renderSenderInfo() { - return isAvatar ? ( + return avatarOwner ? ( ) : ( ); } @@ -384,7 +303,7 @@ const MediaViewer: FC = ({ onForward={handleForward} zoomLevelChange={zoomLevelChange} setZoomLevelChange={setZoomLevelChange} - isAvatar={isAvatar} + isAvatar={Boolean(avatarOwner)} /> = ({ /> @@ -423,9 +340,8 @@ export default memo(withGlobal( const { chatId, threadId, - messageId, + mediaId, avatarOwnerId, - profilePhotoIndex, origin, } = global.mediaViewer; const { @@ -435,18 +351,18 @@ export default memo(withGlobal( let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId); if (origin === MediaViewerOrigin.SearchResult) { - if (!(chatId && messageId)) { + if (!(chatId && mediaId)) { return { animationLevel }; } - const message = selectChatMessage(global, chatId, messageId); + const message = selectChatMessage(global, chatId, mediaId); if (!message) { return { animationLevel }; } return { chatId, - messageId, + mediaId, senderId: message.senderId, isChatWithSelf, origin, @@ -460,25 +376,24 @@ export default memo(withGlobal( isChatWithSelf = selectIsChatWithSelf(global, avatarOwnerId); return { - messageId: -1, + mediaId, senderId: avatarOwnerId, avatarOwner: sender, isChatWithSelf, - profilePhotoIndex: profilePhotoIndex || 0, animationLevel, origin, }; } - if (!(chatId && threadId && messageId)) { + if (!(chatId && threadId && mediaId)) { return { animationLevel }; } let message: ApiMessage | undefined; if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) { - message = selectScheduledMessage(global, chatId, messageId); + message = selectScheduledMessage(global, chatId, mediaId); } else { - message = selectChatMessage(global, chatId, messageId); + message = selectChatMessage(global, chatId, mediaId); } if (!message) { @@ -505,7 +420,7 @@ export default memo(withGlobal( return { chatId, threadId, - messageId, + mediaId, senderId: message.senderId, isChatWithSelf, origin, diff --git a/src/components/mediaViewer/MediaViewerContent.tsx b/src/components/mediaViewer/MediaViewerContent.tsx index aaee11749..1b574a1a7 100644 --- a/src/components/mediaViewer/MediaViewerContent.tsx +++ b/src/components/mediaViewer/MediaViewerContent.tsx @@ -5,37 +5,17 @@ import { withGlobal } from '../../global'; import type { ApiChat, ApiDimensions, ApiMessage, ApiUser, } from '../../api/types'; -import { ApiMediaFormat } from '../../api/types'; import { MediaViewerOrigin } from '../../types'; import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment'; -import useBlurSync from '../../hooks/useBlurSync'; -import useMedia from '../../hooks/useMedia'; -import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress'; -import { - getChatAvatarHash, - getMessageDocument, - getMessageFileSize, - getMessageMediaFormat, - getMessageMediaHash, - getMessageMediaThumbDataUri, - getMessagePhoto, - getMessageVideo, - getMessageWebPagePhoto, - getMessageWebPageVideo, - getPhotoFullDimensions, getVideoAvatarMediaHash, - getVideoDimensions, - isMessageDocumentPhoto, - isMessageDocumentVideo, -} from '../../global/helpers'; import { selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectUser, } from '../../global/selectors'; -import { - AVATAR_FULL_DIMENSIONS, calculateMediaViewerDimensions, VIDEO_AVATAR_FULL_DIMENSIONS, -} from '../common/helpers/mediaDimensions'; +import { calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions'; import { renderMessageText } from '../common/helpers/renderMessageText'; import stopEvent from '../../util/stopEvent'; +import buildClassName from '../../util/buildClassName'; +import { useMediaProps } from './hooks/useMediaProps'; import Spinner from '../ui/Spinner'; import MediaViewerFooter from './MediaViewerFooter'; @@ -44,11 +24,10 @@ import VideoPlayer from './VideoPlayer'; import './MediaViewerContent.scss'; type OwnProps = { - messageId?: number; + mediaId?: number; chatId?: string; threadId?: number; avatarOwnerId?: string; - profilePhotoIndex?: number; origin?: MediaViewerOrigin; isActive?: boolean; animationLevel: 0 | 1 | 2; @@ -60,11 +39,10 @@ type OwnProps = { type StateProps = { chatId?: string; - messageId?: number; + mediaId?: number; senderId?: string; threadId?: number; avatarOwner?: ApiChat | ApiUser; - profilePhotoIndex?: number; message?: ApiMessage; origin?: MediaViewerOrigin; isProtected?: boolean; @@ -77,12 +55,11 @@ const ANIMATION_DURATION = 350; const MediaViewerContent: FC = (props) => { const { - messageId, + mediaId, isActive, avatarOwner, chatId, message, - profilePhotoIndex, origin, animationLevel, isFooterHidden, @@ -94,63 +71,27 @@ const MediaViewerContent: FC = (props) => { onFooterClick, setIsFooterHidden, } = props; - /* Content */ - const photo = message ? getMessagePhoto(message) : undefined; - const video = message ? getMessageVideo(message) : undefined; - const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined; - const webPageVideo = message ? getMessageWebPageVideo(message) : undefined; - const isDocumentPhoto = message ? isMessageDocumentPhoto(message) : false; - const isDocumentVideo = message ? isMessageDocumentVideo(message) : false; - const isVideo = Boolean(video || webPageVideo || isDocumentVideo); - const isPhoto = Boolean(!isVideo && (photo || webPagePhoto || isDocumentPhoto)); - const { isGif } = video || webPageVideo || {}; - - const isOpen = Boolean(avatarOwner || messageId); - const isAvatar = Boolean(avatarOwner); - const isVideoAvatar = isAvatar && avatarOwner.hasVideoAvatar; - - const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia; - const isFromSearch = origin === MediaViewerOrigin.SearchResult; const isGhostAnimation = animationLevel === 2; - /* Media data */ - function getMediaHash(isFull?: boolean) { - if (isAvatar && profilePhotoIndex !== undefined) { - const { photos, hasVideoAvatar } = avatarOwner!; - const avatarPhoto = photos && photos[profilePhotoIndex]; - return avatarPhoto ? (hasVideoAvatar ? getVideoAvatarMediaHash(avatarPhoto) : `photo${avatarPhoto.id}?size=c`) - : getChatAvatarHash(avatarOwner!, isFull ? 'big' : 'normal'); - } - - return message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview'); - } - - const pictogramBlobUrl = useMedia( - message && (isFromSharedMedia || isFromSearch) && getMessageMediaHash(message, 'pictogram'), - undefined, - ApiMediaFormat.BlobUrl, - undefined, - isGhostAnimation && ANIMATION_DURATION, - ); - const previewMediaHash = getMediaHash(); - const previewBlobUrl = useMedia( - previewMediaHash, - undefined, - ApiMediaFormat.BlobUrl, - undefined, - isGhostAnimation && ANIMATION_DURATION, - ); const { - mediaData: fullMediaBlobUrl, + isVideo, + isPhoto, + bestImageData, + dimensions, + isGif, + isVideoAvatar, + localBlobUrl, + fullMediaBlobUrl, + previewBlobUrl, + pictogramBlobUrl, + videoSize, loadProgress, - } = useMediaWithLoadProgress( - getMediaHash(true), - undefined, - message && getMessageMediaFormat(message, 'viewerFull'), - undefined, - isGhostAnimation && ANIMATION_DURATION, - ); + } = useMediaProps({ + message, avatarOwner, mediaId, origin, delay: isGhostAnimation && ANIMATION_DURATION, + }); + + const isOpen = Boolean(avatarOwner || mediaId); const toggleControls = useCallback((isVisible) => { setIsFooterHidden?.(!isVisible); @@ -164,29 +105,7 @@ const MediaViewerContent: FC = (props) => { toggleControls(false); }, [toggleControls]); - const localBlobUrl = (photo || video) ? (photo || video)!.blobUrl : undefined; - let bestImageData = (!isVideo && (localBlobUrl || fullMediaBlobUrl)) || previewBlobUrl || pictogramBlobUrl; - const thumbDataUri = useBlurSync(!bestImageData && message && getMessageMediaThumbDataUri(message)); - if (!bestImageData && origin !== MediaViewerOrigin.SearchResult) { - bestImageData = thumbDataUri; - } - - const videoSize = message ? getMessageFileSize(message) : undefined; - - let dimensions!: ApiDimensions; - if (message) { - if (isDocumentPhoto || isDocumentVideo) { - dimensions = getMessageDocument(message)!.mediaSize!; - } else if (photo || webPagePhoto) { - dimensions = getPhotoFullDimensions((photo || webPagePhoto)!)!; - } else if (video || webPageVideo) { - dimensions = getVideoDimensions((video || webPageVideo)!)!; - } - } else { - dimensions = isVideoAvatar ? VIDEO_AVATAR_FULL_DIMENSIONS : AVATAR_FULL_DIMENSIONS; - } - - if (isAvatar) { + if (avatarOwner) { if (!isVideoAvatar) { return (
@@ -194,6 +113,7 @@ const MediaViewerContent: FC = (props) => { fullMediaBlobUrl || previewBlobUrl, calculateMediaViewerDimensions(dimensions, false), !IS_SINGLE_COLUMN_LAYOUT && !isProtected, + isProtected, )}
); @@ -201,7 +121,7 @@ const MediaViewerContent: FC = (props) => { return (
= (props) => { isMediaViewerOpen={isOpen && isActive} areControlsVisible={!isFooterHidden} toggleControls={toggleControls} + isProtected={isProtected} noPlay={!isActive} onClose={onClose} isMuted @@ -228,23 +149,24 @@ const MediaViewerContent: FC = (props) => { return (
- {isProtected &&
} {isPhoto && renderPhoto( localBlobUrl || fullMediaBlobUrl || previewBlobUrl || pictogramBlobUrl, message && calculateMediaViewerDimensions(dimensions!, hasFooter), !IS_SINGLE_COLUMN_LAYOUT && !isProtected, + isProtected, )} {isVideo && (!isActive ? renderVideoPreview( bestImageData, message && calculateMediaViewerDimensions(dimensions!, hasFooter, true), !IS_SINGLE_COLUMN_LAYOUT && !isProtected, + isProtected, ) : ( = (props) => { noPlay={!isActive} onClose={onClose} isMuted={isMuted} + isProtected={isProtected} volume={volume} playbackRate={playbackRate} /> @@ -265,6 +188,7 @@ const MediaViewerContent: FC = (props) => { @@ -278,9 +202,8 @@ export default memo(withGlobal( const { chatId, threadId, - messageId, + mediaId, avatarOwnerId, - profilePhotoIndex, origin, } = ownProps; @@ -291,18 +214,18 @@ export default memo(withGlobal( } = global.mediaViewer; if (origin === MediaViewerOrigin.SearchResult) { - if (!(chatId && messageId)) { + if (!(chatId && mediaId)) { return { volume, isMuted, playbackRate }; } - const message = selectChatMessage(global, chatId, messageId); + const message = selectChatMessage(global, chatId, mediaId); if (!message) { return { volume, isMuted, playbackRate }; } return { chatId, - messageId, + mediaId, senderId: message.senderId, origin, message, @@ -317,10 +240,9 @@ export default memo(withGlobal( const sender = selectUser(global, avatarOwnerId) || selectChat(global, avatarOwnerId); return { - messageId: -1, + mediaId, senderId: avatarOwnerId, avatarOwner: sender, - profilePhotoIndex: profilePhotoIndex || 0, origin, volume, isMuted, @@ -328,15 +250,15 @@ export default memo(withGlobal( }; } - if (!(chatId && threadId && messageId)) { + if (!(chatId && threadId && mediaId)) { return { volume, isMuted, playbackRate }; } let message: ApiMessage | undefined; if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) { - message = selectScheduledMessage(global, chatId, messageId); + message = selectScheduledMessage(global, chatId, mediaId); } else { - message = selectChatMessage(global, chatId, messageId); + message = selectChatMessage(global, chatId, mediaId); } if (!message) { @@ -346,7 +268,7 @@ export default memo(withGlobal( return { chatId, threadId, - messageId, + mediaId, senderId: message.senderId, origin, message, @@ -358,15 +280,19 @@ export default memo(withGlobal( }, )(MediaViewerContent)); -function renderPhoto(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean) { +function renderPhoto(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean, isProtected?: boolean) { return blobUrl ? ( - +
+ {isProtected &&
} + +
) : (
+ {isProtected &&
}
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
diff --git a/src/components/mediaViewer/MediaViewerFooter.tsx b/src/components/mediaViewer/MediaViewerFooter.tsx index b080b5ef9..0fc7f9168 100644 --- a/src/components/mediaViewer/MediaViewerFooter.tsx +++ b/src/components/mediaViewer/MediaViewerFooter.tsx @@ -17,10 +17,11 @@ type OwnProps = { onClick: () => void; isHidden?: boolean; isForVideo: boolean; + isProtected?: boolean; }; const MediaViewerFooter: FC = ({ - text = '', isHidden, isForVideo, onClick, + text = '', isHidden, isForVideo, onClick, isProtected, }) => { const [isMultiline, setIsMultiline] = useState(false); useEffect(() => { @@ -54,6 +55,7 @@ const MediaViewerFooter: FC = ({ 'MediaViewerFooter', isForVideo && 'is-for-video', isHidden && 'is-hidden', + isProtected && 'is-protected', ); return ( diff --git a/src/components/mediaViewer/MediaViewerSlides.tsx b/src/components/mediaViewer/MediaViewerSlides.tsx index d70fcbac0..e088708c2 100644 --- a/src/components/mediaViewer/MediaViewerSlides.tsx +++ b/src/components/mediaViewer/MediaViewerSlides.tsx @@ -27,18 +27,16 @@ import './MediaViewerSlides.scss'; const { easeOutCubic, easeOutQuart } = timingFunctions; type OwnProps = { - messageId?: number; - getMessageId: (fromId?: number, direction?: number) => number | undefined; + mediaId?: number; + getMediaId: (fromId?: number, direction?: number) => number | undefined; isVideo?: boolean; isGif?: boolean; isPhoto?: boolean; isOpen?: boolean; - selectMessage: (id?: number) => void; + selectMedia: (id?: number) => void; chatId?: string; threadId?: number; - isActive?: boolean; avatarOwnerId?: string; - profilePhotoIndex?: number; origin?: MediaViewerOrigin; animationLevel: 0 | 1 | 2; onClose: () => void; @@ -74,14 +72,13 @@ enum SwipeDirection { } const MediaViewerSlides: FC = ({ - messageId, - getMessageId, - selectMessage, + mediaId, + getMediaId, + selectMedia, isVideo, isGif, isPhoto, isOpen, - isActive, hasFooter, zoomLevelChange, animationLevel, @@ -96,7 +93,7 @@ const MediaViewerSlides: FC = ({ const swipeDirectionRef = useRef(undefined); const isActiveRef = useRef(true); const isReleasedRef = useRef(false); - const [activeMessageId, setActiveMessageId] = useState(messageId); + const [activeMediaId, setActiveMediaId] = useState(mediaId); const prevZoomLevelChange = usePrevious(zoomLevelChange); const hasZoomChanged = prevZoomLevelChange !== undefined && prevZoomLevelChange !== zoomLevelChange; const forceUpdate = useForceUpdate(); @@ -112,7 +109,7 @@ const MediaViewerSlides: FC = ({ forceUpdate(); }, [forceUpdate]); - const selectMessageDebounced = useDebouncedCallback(selectMessage, [], DEBOUNCE_MESSAGE, true); + const selectMediaDebounced = useDebouncedCallback(selectMedia, [], DEBOUNCE_MESSAGE, true); const clearSwipeDirectionDebounced = useDebouncedCallback(() => { swipeDirectionRef.current = undefined; }, [], DEBOUNCE_SWIPE, true); @@ -135,7 +132,7 @@ const MediaViewerSlides: FC = ({ useTimeout(() => setIsFooterHidden(false), ANIMATION_DURATION - 150); useEffect(() => { - if (!containerRef.current || !activeMessageId) { + if (!containerRef.current || activeMediaId === undefined) { return undefined; } let lastTransform = lastTransformRef.current; @@ -159,13 +156,13 @@ const MediaViewerSlides: FC = ({ }, 500, false, true); const changeSlide = (direction: number) => { - const mId = getMessageId(activeMessageId, direction); - if (mId) { + const mId = getMediaId(activeMediaId, direction); + if (mId !== undefined) { const offset = (windowWidth + SLIDES_GAP) * direction; transformRef.current.x += offset; isActiveRef.current = false; - setActiveMessageId(mId); - selectMessageDebounced(mId); + setActiveMediaId(mId); + selectMediaDebounced(mId); setIsActiveDebounced(true); lastTransform = { x: 0, y: 0, scale: 1 }; if (animationLevel === 0) { @@ -347,19 +344,19 @@ const MediaViewerSlides: FC = ({ } // Get horizontal swipe direction const direction = x < 0 ? 1 : -1; - const mId = getMessageId(activeMessageId, x < 0 ? 1 : -1); + const mId = getMediaId(activeMediaId, x < 0 ? 1 : -1); // Get the direction of the last pan gesture. // Could be different from the total horizontal swipe direction // if user starts a swipe in one direction and then changes the direction // we need to cancel slide transition const dirX = panDelta.x < 0 ? -1 : 1; - if (mId && absX >= SWIPE_X_THRESHOLD && direction === dirX) { + if (mId !== undefined && absX >= SWIPE_X_THRESHOLD && direction === dirX) { const offset = (windowWidth + SLIDES_GAP) * direction; // If image is shifted by more than SWIPE_X_THRESHOLD, // We shift everything by one screen width and then set new active message id transformRef.current.x += offset; - setActiveMessageId(mId); - selectMessageDebounced(mId); + setActiveMediaId(mId); + selectMediaDebounced(mId); } // Then we always return to the original position cancelAnimation = animateNumber({ @@ -606,13 +603,13 @@ const MediaViewerSlides: FC = ({ }, [ onClose, setTransform, - getMessageId, - activeMessageId, + getMediaId, + activeMediaId, windowWidth, windowHeight, clickXThreshold, shouldCloseOnVideo, - selectMessageDebounced, + selectMediaDebounced, setIsActiveDebounced, clearSwipeDirectionDebounced, animationLevel, @@ -623,12 +620,13 @@ const MediaViewerSlides: FC = ({ if (!containerRef.current || !hasZoomChanged) return; const { scale } = transformRef.current; const dir = zoomLevelChange > 0 ? -1 : +1; - const minZoom = MIN_ZOOM * 0.5; + const minZoom = MIN_ZOOM * 0.6; const maxZoom = MAX_ZOOM * 3; - const steps = 100; + let steps = 100; let prevValue = 0; if (scale <= minZoom && dir > 0) return; if (scale >= maxZoom && dir < 0) return; + if (scale === 1 && dir > 0) steps = 20; if (cancelZoomAnimation) cancelZoomAnimation(); cancelZoomAnimation = animateNumber({ from: dir, @@ -649,61 +647,61 @@ const MediaViewerSlides: FC = ({ }); }, [zoomLevelChange, hasZoomChanged]); - if (!activeMessageId) return undefined; + if (activeMediaId === undefined) return undefined; - const nextMessageId = getMessageId(activeMessageId, 1); - const previousMessageId = getMessageId(activeMessageId, -1); + const nextMediaId = getMediaId(activeMediaId, 1); + const prevMediaId = getMediaId(activeMediaId, -1); + const hasPrev = prevMediaId !== undefined; + const hasNext = nextMediaId !== undefined; const offsetX = transformRef.current.x; const offsetY = transformRef.current.y; const { scale } = transformRef.current; return (
- {previousMessageId && scale === 1 && !isResizing && ( + {hasPrev && scale === 1 && !isResizing && (
)} - {activeMessageId && ( -
1 && 'MediaViewerSlide--moving', - )} - onClick={handleToggleFooterVisibility} - ref={activeSlideRef} - style={getAnimationStyle(offsetX, offsetY, scale)} - > - -
- )} - {nextMessageId && scale === 1 && !isResizing && ( +
1 && 'MediaViewerSlide--moving', + )} + onClick={handleToggleFooterVisibility} + ref={activeSlideRef} + style={getAnimationStyle(offsetX, offsetY, scale)} + > + +
+ {hasNext && scale === 1 && !isResizing && (
)} - {previousMessageId && scale === 1 && !IS_TOUCH_ENV && ( + {hasPrev && scale === 1 && !IS_TOUCH_ENV && (