Media Viewer: Support switching avatars (#1929)

This commit is contained in:
Alexander Zinchuk 2022-08-05 19:22:43 +02:00
parent 06fe3a2640
commit a55b410e6a
22 changed files with 439 additions and 381 deletions

View File

@ -66,22 +66,25 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
const { const {
loadFullChat, loadFullChat,
openMediaViewer, openMediaViewer,
loadProfilePhotos,
} = getActions(); } = getActions();
const isSuperGroup = chat && isChatSuperGroup(chat); const isSuperGroup = chat && isChatSuperGroup(chat);
const { id: chatId, isMin, isRestricted } = chat || {}; const { id: chatId, isMin, isRestricted } = chat || {};
useEffect(() => { useEffect(() => {
if (chatId && !isMin && withFullInfo && lastSyncTime) { if (chatId && !isMin && lastSyncTime) {
loadFullChat({ chatId }); 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<HTMLDivElement, MouseEvent>, hasMedia: boolean) => { const handleAvatarViewerOpen = useCallback((e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {
if (chat && hasMedia) { if (chat && hasMedia) {
e.stopPropagation(); e.stopPropagation();
openMediaViewer({ openMediaViewer({
avatarOwnerId: chat.id, avatarOwnerId: chat.id,
mediaId: 0,
origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar, origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar,
}); });
} }

View File

@ -67,22 +67,25 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
const { const {
loadFullUser, loadFullUser,
openMediaViewer, openMediaViewer,
loadProfilePhotos,
} = getActions(); } = getActions();
const { id: userId } = user || {}; const { id: userId } = user || {};
const fullName = getUserFullName(user); const fullName = getUserFullName(user);
useEffect(() => { useEffect(() => {
if (withFullInfo && lastSyncTime && userId) { if (userId && lastSyncTime) {
loadFullUser({ userId }); if (withFullInfo) loadFullUser({ userId });
if (withMediaViewer) loadProfilePhotos({ profileId: userId });
} }
}, [userId, loadFullUser, lastSyncTime, withFullInfo]); }, [userId, loadFullUser, loadProfilePhotos, lastSyncTime, withFullInfo, withMediaViewer]);
const handleAvatarViewerOpen = useCallback((e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => { const handleAvatarViewerOpen = useCallback((e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {
if (user && hasMedia) { if (user && hasMedia) {
e.stopPropagation(); e.stopPropagation();
openMediaViewer({ openMediaViewer({
avatarOwnerId: user.id, avatarOwnerId: user.id,
mediaId: 0,
origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar, origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar,
}); });
} }

View File

@ -9,6 +9,7 @@ import type { GlobalState } from '../../global/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { IS_TOUCH_ENV } from '../../util/environment'; import { IS_TOUCH_ENV } from '../../util/environment';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import { selectChat, selectUser, selectUserStatus } from '../../global/selectors'; import { selectChat, selectUser, selectUserStatus } from '../../global/selectors';
import { import {
getUserFullName, getUserStatus, isChatChannel, isUserOnline, getUserFullName, getUserStatus, isChatChannel, isUserOnline,
@ -18,6 +19,7 @@ import { captureEvents, SwipeDirection } from '../../util/captureEvents';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import usePhotosPreload from './hooks/usePhotosPreload'; import usePhotosPreload from './hooks/usePhotosPreload';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import usePrevious from '../../hooks/usePrevious';
import VerifiedIcon from './VerifiedIcon'; import VerifiedIcon from './VerifiedIcon';
import ProfilePhoto from './ProfilePhoto'; import ProfilePhoto from './ProfilePhoto';
@ -40,6 +42,8 @@ type StateProps =
isSavedMessages?: boolean; isSavedMessages?: boolean;
animationLevel: 0 | 1 | 2; animationLevel: 0 | 1 | 2;
serverTimeOffset: number; serverTimeOffset: number;
mediaId?: number;
avatarOwnerId?: string;
} }
& Pick<GlobalState, 'connectionState'>; & Pick<GlobalState, 'connectionState'>;
@ -52,6 +56,8 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
connectionState, connectionState,
animationLevel, animationLevel,
serverTimeOffset, serverTimeOffset,
mediaId,
avatarOwnerId,
}) => { }) => {
const { const {
loadFullUser, loadFullUser,
@ -64,15 +70,26 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
const { id: userId } = user || {}; const { id: userId } = user || {};
const { id: chatId } = chat || {}; const { id: chatId } = chat || {};
const fullName = user ? getUserFullName(user) : (chat ? chat.title : ''); const fullName = user ? getUserFullName(user) : (chat ? chat.title : '');
const photos = user?.photos || chat?.photos || []; const photos = user?.photos || chat?.photos || MEMO_EMPTY_ARRAY;
const slideAnimation = animationLevel >= 1 const prevMediaId = usePrevious(mediaId);
? (lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized') const prevAvatarOwnerId = usePrevious(avatarOwnerId);
const [hasSlideAnimation, setHasSlideAnimation] = useState(true);
const slideAnimation = hasSlideAnimation
? animationLevel >= 1 ? (lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized') : 'none'
: 'none'; : 'none';
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0; const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0;
const isLast = isSavedMessages || photos.length <= 1 || currentPhotoIndex === photos.length - 1; 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 // Deleting the last profile photo may result in an error
useEffect(() => { useEffect(() => {
if (currentPhotoIndex > photos.length) { if (currentPhotoIndex > photos.length) {
@ -91,7 +108,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
const handleProfilePhotoClick = useCallback(() => { const handleProfilePhotoClick = useCallback(() => {
openMediaViewer({ openMediaViewer({
avatarOwnerId: userId || chatId, avatarOwnerId: userId || chatId,
profilePhotoIndex: currentPhotoIndex, mediaId: currentPhotoIndex,
origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar, origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar,
}); });
}, [openMediaViewer, userId, chatId, currentPhotoIndex, forceShowSelf]); }, [openMediaViewer, userId, chatId, currentPhotoIndex, forceShowSelf]);
@ -106,7 +123,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
if (isFirst) { if (isFirst) {
return; return;
} }
setHasSlideAnimation(true);
setCurrentPhotoIndex(currentPhotoIndex - 1); setCurrentPhotoIndex(currentPhotoIndex - 1);
}, [currentPhotoIndex, isFirst]); }, [currentPhotoIndex, isFirst]);
@ -114,7 +131,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
if (isLast) { if (isLast) {
return; return;
} }
setHasSlideAnimation(true);
setCurrentPhotoIndex(currentPhotoIndex + 1); setCurrentPhotoIndex(currentPhotoIndex + 1);
}, [currentPhotoIndex, isLast]); }, [currentPhotoIndex, isLast]);
@ -251,6 +268,7 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, userId); const chat = selectChat(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
const { animationLevel } = global.settings.byKey; const { animationLevel } = global.settings.byKey;
const { mediaId, avatarOwnerId } = global.mediaViewer;
return { return {
connectionState, connectionState,
@ -260,6 +278,8 @@ export default memo(withGlobal<OwnProps>(
isSavedMessages, isSavedMessages,
animationLevel, animationLevel,
serverTimeOffset, serverTimeOffset,
mediaId,
avatarOwnerId,
}; };
}, },
)(ProfileInfo)); )(ProfileInfo));

View File

@ -15,6 +15,7 @@ import {
} from '../../global/helpers'; } from '../../global/helpers';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import safePlay from '../../util/safePlay';
import { getFirstLetters } from '../../util/textFormat'; import { getFirstLetters } from '../../util/textFormat';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -80,7 +81,7 @@ const ProfilePhoto: FC<OwnProps> = ({
videoRef.current.pause(); videoRef.current.pause();
videoRef.current.currentTime = 0; videoRef.current.currentTime = 0;
} else { } else {
videoRef.current.play(); safePlay(videoRef.current);
} }
}, [notActive]); }, [notActive]);

View File

@ -79,10 +79,10 @@ const MediaResults: FC<OwnProps & StateProps> = ({
}).filter(Boolean); }).filter(Boolean);
}, [globalMessagesByChatId, foundIds]); }, [globalMessagesByChatId, foundIds]);
const handleSelectMedia = useCallback((messageId: number, chatId: string) => { const handleSelectMedia = useCallback((id: number, chatId: string) => {
openMediaViewer({ openMediaViewer({
chatId, chatId,
messageId, mediaId: id,
origin: MediaViewerOrigin.SearchResult, origin: MediaViewerOrigin.SearchResult,
}); });
}, [openMediaViewer]); }, [openMediaViewer]);

View File

@ -176,6 +176,12 @@
} }
} }
} }
.is-protected {
user-select: none;
-webkit-touch-callout: none;
pointer-events: none;
}
} }
.ghost { .ghost {

View File

@ -4,28 +4,13 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import type { import type {
ApiChat, ApiDimensions, ApiMessage, ApiUser, ApiChat, ApiMessage, ApiUser,
} from '../../api/types'; } from '../../api/types';
import { ApiMediaFormat } from '../../api/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import { import {
getChatAvatarHash,
getChatMediaMessageIds, getChatMediaMessageIds,
getMessageDocument,
getMessageFileName,
getMessageMediaFormat,
getMessageMediaHash,
getMessageMediaThumbDataUri,
getMessagePhoto,
getMessageVideo,
getMessageWebPagePhoto,
getMessageWebPageVideo,
getPhotoFullDimensions, getVideoAvatarMediaHash,
getVideoDimensions,
isMessageDocumentPhoto,
isMessageDocumentVideo,
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
selectChat, selectChat,
@ -43,22 +28,18 @@ import { stopCurrentAudio } from '../../util/audioPlayer';
import captureEscKeyListener from '../../util/captureEscKeyListener'; import captureEscKeyListener from '../../util/captureEscKeyListener';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { ANIMATION_END_DELAY } from '../../config'; import { ANIMATION_END_DELAY } from '../../config';
import { import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions';
AVATAR_FULL_DIMENSIONS, MEDIA_VIEWER_MEDIA_QUERY, VIDEO_AVATAR_FULL_DIMENSIONS,
} from '../common/helpers/mediaDimensions';
import windowSize from '../../util/windowSize'; import windowSize from '../../util/windowSize';
import { animateClosing, animateOpening } from './helpers/ghostAnimation'; import { animateClosing, animateOpening } from './helpers/ghostAnimation';
import { renderMessageText } from '../common/helpers/renderMessageText'; import { renderMessageText } from '../common/helpers/renderMessageText';
import useBlurSync from '../../hooks/useBlurSync';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useForceUpdate from '../../hooks/useForceUpdate'; import useForceUpdate from '../../hooks/useForceUpdate';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import useHistoryBack from '../../hooks/useHistoryBack'; import useHistoryBack from '../../hooks/useHistoryBack';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useMedia from '../../hooks/useMedia';
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import { useMediaProps } from './hooks/useMediaProps';
import ReportModal from '../common/ReportModal'; import ReportModal from '../common/ReportModal';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -73,12 +54,11 @@ import './MediaViewer.scss';
type StateProps = { type StateProps = {
chatId?: string; chatId?: string;
threadId?: number; threadId?: number;
messageId?: number; mediaId?: number;
senderId?: string; senderId?: string;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
avatarOwner?: ApiChat | ApiUser; avatarOwner?: ApiChat | ApiUser;
profilePhotoIndex?: number;
message?: ApiMessage; message?: ApiMessage;
chatMessages?: Record<number, ApiMessage>; chatMessages?: Record<number, ApiMessage>;
collectionIds?: number[]; collectionIds?: number[];
@ -90,12 +70,11 @@ const ANIMATION_DURATION = 350;
const MediaViewer: FC<StateProps> = ({ const MediaViewer: FC<StateProps> = ({
chatId, chatId,
threadId, threadId,
messageId, mediaId,
senderId, senderId,
isChatWithSelf, isChatWithSelf,
origin, origin,
avatarOwner, avatarOwner,
profilePhotoIndex,
message, message,
chatMessages, chatMessages,
collectionIds, collectionIds,
@ -109,40 +88,11 @@ const MediaViewer: FC<StateProps> = ({
toggleChatInfo, toggleChatInfo,
} = getActions(); } = getActions();
const isOpen = Boolean(avatarOwner || messageId); const isOpen = Boolean(avatarOwner || mediaId);
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;
/* Animation */ /* Animation */
const animationKey = useRef<number>(); const animationKey = useRef<number>();
const prevSenderId = usePrevious<string | undefined>(senderId); const prevSenderId = usePrevious<string | undefined>(senderId);
if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) {
animationKey.current = selectedMediaMessageIndex;
}
const headerAnimation = animationLevel === 2 ? 'slide-fade' : 'none'; const headerAnimation = animationLevel === 2 ? 'slide-fade' : 'none';
const isGhostAnimation = animationLevel === 2; const isGhostAnimation = animationLevel === 2;
@ -150,80 +100,46 @@ const MediaViewer: FC<StateProps> = ({
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(); const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
const [zoomLevelChange, setZoomLevelChange] = useState<number>(1); const [zoomLevelChange, setZoomLevelChange] = useState<number>(1);
/* Media data */ const {
function getMediaHash(isFull?: boolean) { webPagePhoto,
if (isAvatar && profilePhotoIndex !== undefined) { webPageVideo,
const { photos } = avatarOwner!; isVideo,
const avatarPhoto = photos && photos[profilePhotoIndex]; isPhoto,
return avatarPhoto bestImageData,
// Video for avatar should be used only for full size dimensions,
? (avatarPhoto.isVideo && isFull ? getVideoAvatarMediaHash(avatarPhoto) : `photo${avatarPhoto.id}?size=c`) isGif,
: getChatAvatarHash(avatarOwner!, isFull ? 'big' : 'normal'); 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( if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) {
message && (isFromSharedMedia || isFromSearch) && getMessageMediaHash(message, 'pictogram'), animationKey.current = selectedMediaIndex;
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;
} }
useEffect(() => { useEffect(() => {
if (!IS_SINGLE_COLUMN_LAYOUT) { if (!IS_SINGLE_COLUMN_LAYOUT) return;
return;
}
document.body.classList.toggle('is-media-viewer-open', isOpen); document.body.classList.toggle('is-media-viewer-open', isOpen);
}, [isOpen]); }, [isOpen]);
@ -277,28 +193,31 @@ const MediaViewer: FC<StateProps> = ({
if (IS_SINGLE_COLUMN_LAYOUT) { if (IS_SINGLE_COLUMN_LAYOUT) {
setTimeout(() => { setTimeout(() => {
toggleChatInfo(false, { forceSyncOnIOs: true }); toggleChatInfo(false, { forceSyncOnIOs: true });
focusMessage({ chatId, threadId, messageId }); focusMessage({ chatId, threadId, mediaId });
}, ANIMATION_DURATION); }, ANIMATION_DURATION);
} else { } else {
focusMessage({ chatId, threadId, messageId }); focusMessage({ chatId, threadId, mediaId });
} }
}, [close, chatId, threadId, focusMessage, toggleChatInfo, messageId]); }, [close, chatId, threadId, focusMessage, toggleChatInfo, mediaId]);
const handleForward = useCallback(() => { const handleForward = useCallback(() => {
openForwardMenu({ openForwardMenu({
fromChatId: chatId, fromChatId: chatId,
messageIds: [messageId], messageIds: [mediaId],
}); });
}, [openForwardMenu, chatId, messageId]); }, [openForwardMenu, chatId, mediaId]);
const selectMessage = useCallback((id?: number) => openMediaViewer({ const selectMedia = useCallback((id?: number) => {
chatId, openMediaViewer({
threadId, chatId,
messageId: id, threadId,
origin, mediaId: id,
}, { avatarOwnerId: avatarOwner?.id,
forceOnHeavyAnimation: true, origin,
}), [chatId, openMediaViewer, origin, threadId]); }, {
forceOnHeavyAnimation: true,
});
}, [avatarOwner?.id, chatId, openMediaViewer, origin, threadId]);
useEffect(() => (isOpen ? captureEscKeyListener(() => { useEffect(() => (isOpen ? captureEscKeyListener(() => {
close(); close();
@ -323,14 +242,14 @@ const MediaViewer: FC<StateProps> = ({
}; };
}, [isOpen]); }, [isOpen]);
const getMessageId = useCallback((fromId?: number, direction?: number): number | undefined => { const getMediaId = useCallback((fromId?: number, direction?: number): number | undefined => {
if (!fromId) return undefined; if (fromId === undefined) return undefined;
const index = messageIds.indexOf(fromId); const index = mediaIds.indexOf(fromId);
if ((direction === -1 && index > 0) || (direction === 1 && index < messageIds.length - 1)) { if ((direction === -1 && index > 0) || (direction === 1 && index < mediaIds.length - 1)) {
return messageIds[index + direction]; return mediaIds[index + direction];
} }
return undefined; return undefined;
}, [messageIds]); }, [mediaIds]);
const lang = useLang(); const lang = useLang();
@ -340,17 +259,17 @@ const MediaViewer: FC<StateProps> = ({
}); });
function renderSenderInfo() { function renderSenderInfo() {
return isAvatar ? ( return avatarOwner ? (
<SenderInfo <SenderInfo
key={avatarOwner!.id} key={avatarOwner.id}
chatId={avatarOwner!.id} chatId={avatarOwner.id}
isAvatar isAvatar
/> />
) : ( ) : (
<SenderInfo <SenderInfo
key={messageId} key={mediaId}
chatId={chatId} chatId={chatId}
messageId={messageId} messageId={mediaId}
/> />
); );
} }
@ -384,7 +303,7 @@ const MediaViewer: FC<StateProps> = ({
onForward={handleForward} onForward={handleForward}
zoomLevelChange={zoomLevelChange} zoomLevelChange={zoomLevelChange}
setZoomLevelChange={setZoomLevelChange} setZoomLevelChange={setZoomLevelChange}
isAvatar={isAvatar} isAvatar={Boolean(avatarOwner)}
/> />
<ReportModal <ReportModal
isOpen={isReportModalOpen} isOpen={isReportModalOpen}
@ -395,23 +314,21 @@ const MediaViewer: FC<StateProps> = ({
/> />
</div> </div>
<MediaViewerSlides <MediaViewerSlides
messageId={messageId} mediaId={mediaId}
getMessageId={getMessageId} getMediaId={getMediaId}
chatId={chatId} chatId={chatId}
isPhoto={isPhoto} isPhoto={isPhoto}
isGif={isGif} isGif={isGif}
threadId={threadId} threadId={threadId}
avatarOwnerId={avatarOwner && avatarOwner.id} avatarOwnerId={avatarOwner?.id}
profilePhotoIndex={profilePhotoIndex}
origin={origin} origin={origin}
isOpen={isOpen} isOpen={isOpen}
hasFooter={hasFooter} hasFooter={hasFooter}
zoomLevelChange={zoomLevelChange} zoomLevelChange={zoomLevelChange}
isActive
isVideo={isVideo} isVideo={isVideo}
animationLevel={animationLevel} animationLevel={animationLevel}
onClose={close} onClose={close}
selectMessage={selectMessage} selectMedia={selectMedia}
onFooterClick={handleFooterClick} onFooterClick={handleFooterClick}
/> />
</ShowTransition> </ShowTransition>
@ -423,9 +340,8 @@ export default memo(withGlobal(
const { const {
chatId, chatId,
threadId, threadId,
messageId, mediaId,
avatarOwnerId, avatarOwnerId,
profilePhotoIndex,
origin, origin,
} = global.mediaViewer; } = global.mediaViewer;
const { const {
@ -435,18 +351,18 @@ export default memo(withGlobal(
let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId); let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId);
if (origin === MediaViewerOrigin.SearchResult) { if (origin === MediaViewerOrigin.SearchResult) {
if (!(chatId && messageId)) { if (!(chatId && mediaId)) {
return { animationLevel }; return { animationLevel };
} }
const message = selectChatMessage(global, chatId, messageId); const message = selectChatMessage(global, chatId, mediaId);
if (!message) { if (!message) {
return { animationLevel }; return { animationLevel };
} }
return { return {
chatId, chatId,
messageId, mediaId,
senderId: message.senderId, senderId: message.senderId,
isChatWithSelf, isChatWithSelf,
origin, origin,
@ -460,25 +376,24 @@ export default memo(withGlobal(
isChatWithSelf = selectIsChatWithSelf(global, avatarOwnerId); isChatWithSelf = selectIsChatWithSelf(global, avatarOwnerId);
return { return {
messageId: -1, mediaId,
senderId: avatarOwnerId, senderId: avatarOwnerId,
avatarOwner: sender, avatarOwner: sender,
isChatWithSelf, isChatWithSelf,
profilePhotoIndex: profilePhotoIndex || 0,
animationLevel, animationLevel,
origin, origin,
}; };
} }
if (!(chatId && threadId && messageId)) { if (!(chatId && threadId && mediaId)) {
return { animationLevel }; return { animationLevel };
} }
let message: ApiMessage | undefined; let message: ApiMessage | undefined;
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) { if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
message = selectScheduledMessage(global, chatId, messageId); message = selectScheduledMessage(global, chatId, mediaId);
} else { } else {
message = selectChatMessage(global, chatId, messageId); message = selectChatMessage(global, chatId, mediaId);
} }
if (!message) { if (!message) {
@ -505,7 +420,7 @@ export default memo(withGlobal(
return { return {
chatId, chatId,
threadId, threadId,
messageId, mediaId,
senderId: message.senderId, senderId: message.senderId,
isChatWithSelf, isChatWithSelf,
origin, origin,

View File

@ -5,37 +5,17 @@ import { withGlobal } from '../../global';
import type { import type {
ApiChat, ApiDimensions, ApiMessage, ApiUser, ApiChat, ApiDimensions, ApiMessage, ApiUser,
} from '../../api/types'; } from '../../api/types';
import { ApiMediaFormat } from '../../api/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment'; 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 { import {
selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectUser, selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectUser,
} from '../../global/selectors'; } from '../../global/selectors';
import { import { calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions';
AVATAR_FULL_DIMENSIONS, calculateMediaViewerDimensions, VIDEO_AVATAR_FULL_DIMENSIONS,
} from '../common/helpers/mediaDimensions';
import { renderMessageText } from '../common/helpers/renderMessageText'; import { renderMessageText } from '../common/helpers/renderMessageText';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../util/stopEvent';
import buildClassName from '../../util/buildClassName';
import { useMediaProps } from './hooks/useMediaProps';
import Spinner from '../ui/Spinner'; import Spinner from '../ui/Spinner';
import MediaViewerFooter from './MediaViewerFooter'; import MediaViewerFooter from './MediaViewerFooter';
@ -44,11 +24,10 @@ import VideoPlayer from './VideoPlayer';
import './MediaViewerContent.scss'; import './MediaViewerContent.scss';
type OwnProps = { type OwnProps = {
messageId?: number; mediaId?: number;
chatId?: string; chatId?: string;
threadId?: number; threadId?: number;
avatarOwnerId?: string; avatarOwnerId?: string;
profilePhotoIndex?: number;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
isActive?: boolean; isActive?: boolean;
animationLevel: 0 | 1 | 2; animationLevel: 0 | 1 | 2;
@ -60,11 +39,10 @@ type OwnProps = {
type StateProps = { type StateProps = {
chatId?: string; chatId?: string;
messageId?: number; mediaId?: number;
senderId?: string; senderId?: string;
threadId?: number; threadId?: number;
avatarOwner?: ApiChat | ApiUser; avatarOwner?: ApiChat | ApiUser;
profilePhotoIndex?: number;
message?: ApiMessage; message?: ApiMessage;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
isProtected?: boolean; isProtected?: boolean;
@ -77,12 +55,11 @@ const ANIMATION_DURATION = 350;
const MediaViewerContent: FC<OwnProps & StateProps> = (props) => { const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
const { const {
messageId, mediaId,
isActive, isActive,
avatarOwner, avatarOwner,
chatId, chatId,
message, message,
profilePhotoIndex,
origin, origin,
animationLevel, animationLevel,
isFooterHidden, isFooterHidden,
@ -94,63 +71,27 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
onFooterClick, onFooterClick,
setIsFooterHidden, setIsFooterHidden,
} = props; } = 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; 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 { const {
mediaData: fullMediaBlobUrl, isVideo,
isPhoto,
bestImageData,
dimensions,
isGif,
isVideoAvatar,
localBlobUrl,
fullMediaBlobUrl,
previewBlobUrl,
pictogramBlobUrl,
videoSize,
loadProgress, loadProgress,
} = useMediaWithLoadProgress( } = useMediaProps({
getMediaHash(true), message, avatarOwner, mediaId, origin, delay: isGhostAnimation && ANIMATION_DURATION,
undefined, });
message && getMessageMediaFormat(message, 'viewerFull'),
undefined, const isOpen = Boolean(avatarOwner || mediaId);
isGhostAnimation && ANIMATION_DURATION,
);
const toggleControls = useCallback((isVisible) => { const toggleControls = useCallback((isVisible) => {
setIsFooterHidden?.(!isVisible); setIsFooterHidden?.(!isVisible);
@ -164,29 +105,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
toggleControls(false); toggleControls(false);
}, [toggleControls]); }, [toggleControls]);
const localBlobUrl = (photo || video) ? (photo || video)!.blobUrl : undefined; if (avatarOwner) {
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 (!isVideoAvatar) { if (!isVideoAvatar) {
return ( return (
<div key={chatId} className="MediaViewerContent"> <div key={chatId} className="MediaViewerContent">
@ -194,6 +113,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
fullMediaBlobUrl || previewBlobUrl, fullMediaBlobUrl || previewBlobUrl,
calculateMediaViewerDimensions(dimensions, false), calculateMediaViewerDimensions(dimensions, false),
!IS_SINGLE_COLUMN_LAYOUT && !isProtected, !IS_SINGLE_COLUMN_LAYOUT && !isProtected,
isProtected,
)} )}
</div> </div>
); );
@ -201,7 +121,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
return ( return (
<div key={chatId} className="MediaViewerContent"> <div key={chatId} className="MediaViewerContent">
<VideoPlayer <VideoPlayer
key={messageId} key={mediaId}
url={localBlobUrl || fullMediaBlobUrl} url={localBlobUrl || fullMediaBlobUrl}
isGif isGif
posterData={bestImageData} posterData={bestImageData}
@ -211,6 +131,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
isMediaViewerOpen={isOpen && isActive} isMediaViewerOpen={isOpen && isActive}
areControlsVisible={!isFooterHidden} areControlsVisible={!isFooterHidden}
toggleControls={toggleControls} toggleControls={toggleControls}
isProtected={isProtected}
noPlay={!isActive} noPlay={!isActive}
onClose={onClose} onClose={onClose}
isMuted isMuted
@ -228,23 +149,24 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
return ( return (
<div <div
className={`MediaViewerContent ${hasFooter ? 'has-footer' : ''}`} className={buildClassName('MediaViewerContent', hasFooter && 'has-footer')}
onMouseMove={!isGif && !IS_TOUCH_ENV ? handleMouseMove : undefined} onMouseMove={!isGif && !IS_TOUCH_ENV ? handleMouseMove : undefined}
onMouseOut={!isGif && !IS_TOUCH_ENV ? handleMouseOut : undefined} onMouseOut={!isGif && !IS_TOUCH_ENV ? handleMouseOut : undefined}
> >
{isProtected && <div onContextMenu={stopEvent} className="protector" />}
{isPhoto && renderPhoto( {isPhoto && renderPhoto(
localBlobUrl || fullMediaBlobUrl || previewBlobUrl || pictogramBlobUrl, localBlobUrl || fullMediaBlobUrl || previewBlobUrl || pictogramBlobUrl,
message && calculateMediaViewerDimensions(dimensions!, hasFooter), message && calculateMediaViewerDimensions(dimensions!, hasFooter),
!IS_SINGLE_COLUMN_LAYOUT && !isProtected, !IS_SINGLE_COLUMN_LAYOUT && !isProtected,
isProtected,
)} )}
{isVideo && (!isActive ? renderVideoPreview( {isVideo && (!isActive ? renderVideoPreview(
bestImageData, bestImageData,
message && calculateMediaViewerDimensions(dimensions!, hasFooter, true), message && calculateMediaViewerDimensions(dimensions!, hasFooter, true),
!IS_SINGLE_COLUMN_LAYOUT && !isProtected, !IS_SINGLE_COLUMN_LAYOUT && !isProtected,
isProtected,
) : ( ) : (
<VideoPlayer <VideoPlayer
key={messageId} key={mediaId}
url={localBlobUrl || fullMediaBlobUrl} url={localBlobUrl || fullMediaBlobUrl}
isGif={isGif} isGif={isGif}
posterData={bestImageData} posterData={bestImageData}
@ -257,6 +179,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
noPlay={!isActive} noPlay={!isActive}
onClose={onClose} onClose={onClose}
isMuted={isMuted} isMuted={isMuted}
isProtected={isProtected}
volume={volume} volume={volume}
playbackRate={playbackRate} playbackRate={playbackRate}
/> />
@ -265,6 +188,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
<MediaViewerFooter <MediaViewerFooter
text={textParts} text={textParts}
onClick={onFooterClick} onClick={onFooterClick}
isProtected={isProtected}
isHidden={isFooterHidden} isHidden={isFooterHidden}
isForVideo={isVideo && !isGif} isForVideo={isVideo && !isGif}
/> />
@ -278,9 +202,8 @@ export default memo(withGlobal<OwnProps>(
const { const {
chatId, chatId,
threadId, threadId,
messageId, mediaId,
avatarOwnerId, avatarOwnerId,
profilePhotoIndex,
origin, origin,
} = ownProps; } = ownProps;
@ -291,18 +214,18 @@ export default memo(withGlobal<OwnProps>(
} = global.mediaViewer; } = global.mediaViewer;
if (origin === MediaViewerOrigin.SearchResult) { if (origin === MediaViewerOrigin.SearchResult) {
if (!(chatId && messageId)) { if (!(chatId && mediaId)) {
return { volume, isMuted, playbackRate }; return { volume, isMuted, playbackRate };
} }
const message = selectChatMessage(global, chatId, messageId); const message = selectChatMessage(global, chatId, mediaId);
if (!message) { if (!message) {
return { volume, isMuted, playbackRate }; return { volume, isMuted, playbackRate };
} }
return { return {
chatId, chatId,
messageId, mediaId,
senderId: message.senderId, senderId: message.senderId,
origin, origin,
message, message,
@ -317,10 +240,9 @@ export default memo(withGlobal<OwnProps>(
const sender = selectUser(global, avatarOwnerId) || selectChat(global, avatarOwnerId); const sender = selectUser(global, avatarOwnerId) || selectChat(global, avatarOwnerId);
return { return {
messageId: -1, mediaId,
senderId: avatarOwnerId, senderId: avatarOwnerId,
avatarOwner: sender, avatarOwner: sender,
profilePhotoIndex: profilePhotoIndex || 0,
origin, origin,
volume, volume,
isMuted, isMuted,
@ -328,15 +250,15 @@ export default memo(withGlobal<OwnProps>(
}; };
} }
if (!(chatId && threadId && messageId)) { if (!(chatId && threadId && mediaId)) {
return { volume, isMuted, playbackRate }; return { volume, isMuted, playbackRate };
} }
let message: ApiMessage | undefined; let message: ApiMessage | undefined;
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) { if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
message = selectScheduledMessage(global, chatId, messageId); message = selectScheduledMessage(global, chatId, mediaId);
} else { } else {
message = selectChatMessage(global, chatId, messageId); message = selectChatMessage(global, chatId, mediaId);
} }
if (!message) { if (!message) {
@ -346,7 +268,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
chatId, chatId,
threadId, threadId,
messageId, mediaId,
senderId: message.senderId, senderId: message.senderId,
origin, origin,
message, message,
@ -358,15 +280,19 @@ export default memo(withGlobal<OwnProps>(
}, },
)(MediaViewerContent)); )(MediaViewerContent));
function renderPhoto(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean) { function renderPhoto(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean, isProtected?: boolean) {
return blobUrl return blobUrl
? ( ? (
<img <div style="position: relative;">
src={blobUrl} {isProtected && <div onContextMenu={stopEvent} className="protector" />}
alt="" <img
style={imageSize ? `width: ${imageSize.width}px` : ''} src={blobUrl}
draggable={Boolean(canDrag)} alt=""
/> className={buildClassName(isProtected && 'is-protected')}
style={imageSize ? `width: ${imageSize.width}px` : ''}
draggable={Boolean(canDrag)}
/>
</div>
) )
: ( : (
<div <div
@ -378,7 +304,7 @@ function renderPhoto(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: bool
); );
} }
function renderVideoPreview(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean) { function renderVideoPreview(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean, isProtected?: boolean) {
const wrapperStyle = imageSize && `width: ${imageSize.width}px; height: ${imageSize.height}px`; const wrapperStyle = imageSize && `width: ${imageSize.width}px; height: ${imageSize.height}px`;
const videoStyle = `background-image: url(${blobUrl})`; const videoStyle = `background-image: url(${blobUrl})`;
return blobUrl return blobUrl
@ -386,12 +312,14 @@ function renderVideoPreview(blobUrl?: string, imageSize?: ApiDimensions, canDrag
<div <div
className="VideoPlayer" className="VideoPlayer"
> >
{isProtected && <div onContextMenu={stopEvent} className="protector" />}
<div <div
style={wrapperStyle} style={wrapperStyle}
> >
{/* eslint-disable-next-line jsx-a11y/media-has-caption */} {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video <video
style={videoStyle} style={videoStyle}
className={buildClassName(isProtected && 'is-protected')}
draggable={Boolean(canDrag)} draggable={Boolean(canDrag)}
/> />
</div> </div>

View File

@ -17,10 +17,11 @@ type OwnProps = {
onClick: () => void; onClick: () => void;
isHidden?: boolean; isHidden?: boolean;
isForVideo: boolean; isForVideo: boolean;
isProtected?: boolean;
}; };
const MediaViewerFooter: FC<OwnProps> = ({ const MediaViewerFooter: FC<OwnProps> = ({
text = '', isHidden, isForVideo, onClick, text = '', isHidden, isForVideo, onClick, isProtected,
}) => { }) => {
const [isMultiline, setIsMultiline] = useState(false); const [isMultiline, setIsMultiline] = useState(false);
useEffect(() => { useEffect(() => {
@ -54,6 +55,7 @@ const MediaViewerFooter: FC<OwnProps> = ({
'MediaViewerFooter', 'MediaViewerFooter',
isForVideo && 'is-for-video', isForVideo && 'is-for-video',
isHidden && 'is-hidden', isHidden && 'is-hidden',
isProtected && 'is-protected',
); );
return ( return (

View File

@ -27,18 +27,16 @@ import './MediaViewerSlides.scss';
const { easeOutCubic, easeOutQuart } = timingFunctions; const { easeOutCubic, easeOutQuart } = timingFunctions;
type OwnProps = { type OwnProps = {
messageId?: number; mediaId?: number;
getMessageId: (fromId?: number, direction?: number) => number | undefined; getMediaId: (fromId?: number, direction?: number) => number | undefined;
isVideo?: boolean; isVideo?: boolean;
isGif?: boolean; isGif?: boolean;
isPhoto?: boolean; isPhoto?: boolean;
isOpen?: boolean; isOpen?: boolean;
selectMessage: (id?: number) => void; selectMedia: (id?: number) => void;
chatId?: string; chatId?: string;
threadId?: number; threadId?: number;
isActive?: boolean;
avatarOwnerId?: string; avatarOwnerId?: string;
profilePhotoIndex?: number;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
animationLevel: 0 | 1 | 2; animationLevel: 0 | 1 | 2;
onClose: () => void; onClose: () => void;
@ -74,14 +72,13 @@ enum SwipeDirection {
} }
const MediaViewerSlides: FC<OwnProps> = ({ const MediaViewerSlides: FC<OwnProps> = ({
messageId, mediaId,
getMessageId, getMediaId,
selectMessage, selectMedia,
isVideo, isVideo,
isGif, isGif,
isPhoto, isPhoto,
isOpen, isOpen,
isActive,
hasFooter, hasFooter,
zoomLevelChange, zoomLevelChange,
animationLevel, animationLevel,
@ -96,7 +93,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
const swipeDirectionRef = useRef<SwipeDirection | undefined>(undefined); const swipeDirectionRef = useRef<SwipeDirection | undefined>(undefined);
const isActiveRef = useRef(true); const isActiveRef = useRef(true);
const isReleasedRef = useRef(false); const isReleasedRef = useRef(false);
const [activeMessageId, setActiveMessageId] = useState<number | undefined>(messageId); const [activeMediaId, setActiveMediaId] = useState<number | undefined>(mediaId);
const prevZoomLevelChange = usePrevious(zoomLevelChange); const prevZoomLevelChange = usePrevious(zoomLevelChange);
const hasZoomChanged = prevZoomLevelChange !== undefined && prevZoomLevelChange !== zoomLevelChange; const hasZoomChanged = prevZoomLevelChange !== undefined && prevZoomLevelChange !== zoomLevelChange;
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
@ -112,7 +109,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
forceUpdate(); forceUpdate();
}, [forceUpdate]); }, [forceUpdate]);
const selectMessageDebounced = useDebouncedCallback(selectMessage, [], DEBOUNCE_MESSAGE, true); const selectMediaDebounced = useDebouncedCallback(selectMedia, [], DEBOUNCE_MESSAGE, true);
const clearSwipeDirectionDebounced = useDebouncedCallback(() => { const clearSwipeDirectionDebounced = useDebouncedCallback(() => {
swipeDirectionRef.current = undefined; swipeDirectionRef.current = undefined;
}, [], DEBOUNCE_SWIPE, true); }, [], DEBOUNCE_SWIPE, true);
@ -135,7 +132,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
useTimeout(() => setIsFooterHidden(false), ANIMATION_DURATION - 150); useTimeout(() => setIsFooterHidden(false), ANIMATION_DURATION - 150);
useEffect(() => { useEffect(() => {
if (!containerRef.current || !activeMessageId) { if (!containerRef.current || activeMediaId === undefined) {
return undefined; return undefined;
} }
let lastTransform = lastTransformRef.current; let lastTransform = lastTransformRef.current;
@ -159,13 +156,13 @@ const MediaViewerSlides: FC<OwnProps> = ({
}, 500, false, true); }, 500, false, true);
const changeSlide = (direction: number) => { const changeSlide = (direction: number) => {
const mId = getMessageId(activeMessageId, direction); const mId = getMediaId(activeMediaId, direction);
if (mId) { if (mId !== undefined) {
const offset = (windowWidth + SLIDES_GAP) * direction; const offset = (windowWidth + SLIDES_GAP) * direction;
transformRef.current.x += offset; transformRef.current.x += offset;
isActiveRef.current = false; isActiveRef.current = false;
setActiveMessageId(mId); setActiveMediaId(mId);
selectMessageDebounced(mId); selectMediaDebounced(mId);
setIsActiveDebounced(true); setIsActiveDebounced(true);
lastTransform = { x: 0, y: 0, scale: 1 }; lastTransform = { x: 0, y: 0, scale: 1 };
if (animationLevel === 0) { if (animationLevel === 0) {
@ -347,19 +344,19 @@ const MediaViewerSlides: FC<OwnProps> = ({
} }
// Get horizontal swipe direction // Get horizontal swipe direction
const direction = x < 0 ? 1 : -1; 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. // Get the direction of the last pan gesture.
// Could be different from the total horizontal swipe direction // Could be different from the total horizontal swipe direction
// if user starts a swipe in one direction and then changes the direction // if user starts a swipe in one direction and then changes the direction
// we need to cancel slide transition // we need to cancel slide transition
const dirX = panDelta.x < 0 ? -1 : 1; 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; const offset = (windowWidth + SLIDES_GAP) * direction;
// If image is shifted by more than SWIPE_X_THRESHOLD, // If image is shifted by more than SWIPE_X_THRESHOLD,
// We shift everything by one screen width and then set new active message id // We shift everything by one screen width and then set new active message id
transformRef.current.x += offset; transformRef.current.x += offset;
setActiveMessageId(mId); setActiveMediaId(mId);
selectMessageDebounced(mId); selectMediaDebounced(mId);
} }
// Then we always return to the original position // Then we always return to the original position
cancelAnimation = animateNumber({ cancelAnimation = animateNumber({
@ -606,13 +603,13 @@ const MediaViewerSlides: FC<OwnProps> = ({
}, [ }, [
onClose, onClose,
setTransform, setTransform,
getMessageId, getMediaId,
activeMessageId, activeMediaId,
windowWidth, windowWidth,
windowHeight, windowHeight,
clickXThreshold, clickXThreshold,
shouldCloseOnVideo, shouldCloseOnVideo,
selectMessageDebounced, selectMediaDebounced,
setIsActiveDebounced, setIsActiveDebounced,
clearSwipeDirectionDebounced, clearSwipeDirectionDebounced,
animationLevel, animationLevel,
@ -623,12 +620,13 @@ const MediaViewerSlides: FC<OwnProps> = ({
if (!containerRef.current || !hasZoomChanged) return; if (!containerRef.current || !hasZoomChanged) return;
const { scale } = transformRef.current; const { scale } = transformRef.current;
const dir = zoomLevelChange > 0 ? -1 : +1; const dir = zoomLevelChange > 0 ? -1 : +1;
const minZoom = MIN_ZOOM * 0.5; const minZoom = MIN_ZOOM * 0.6;
const maxZoom = MAX_ZOOM * 3; const maxZoom = MAX_ZOOM * 3;
const steps = 100; let steps = 100;
let prevValue = 0; let prevValue = 0;
if (scale <= minZoom && dir > 0) return; if (scale <= minZoom && dir > 0) return;
if (scale >= maxZoom && dir < 0) return; if (scale >= maxZoom && dir < 0) return;
if (scale === 1 && dir > 0) steps = 20;
if (cancelZoomAnimation) cancelZoomAnimation(); if (cancelZoomAnimation) cancelZoomAnimation();
cancelZoomAnimation = animateNumber({ cancelZoomAnimation = animateNumber({
from: dir, from: dir,
@ -649,61 +647,61 @@ const MediaViewerSlides: FC<OwnProps> = ({
}); });
}, [zoomLevelChange, hasZoomChanged]); }, [zoomLevelChange, hasZoomChanged]);
if (!activeMessageId) return undefined; if (activeMediaId === undefined) return undefined;
const nextMessageId = getMessageId(activeMessageId, 1); const nextMediaId = getMediaId(activeMediaId, 1);
const previousMessageId = getMessageId(activeMessageId, -1); const prevMediaId = getMediaId(activeMediaId, -1);
const hasPrev = prevMediaId !== undefined;
const hasNext = nextMediaId !== undefined;
const offsetX = transformRef.current.x; const offsetX = transformRef.current.x;
const offsetY = transformRef.current.y; const offsetY = transformRef.current.y;
const { scale } = transformRef.current; const { scale } = transformRef.current;
return ( return (
<div className="MediaViewerSlides" ref={containerRef}> <div className="MediaViewerSlides" ref={containerRef}>
{previousMessageId && scale === 1 && !isResizing && ( {hasPrev && scale === 1 && !isResizing && (
<div className="MediaViewerSlide" style={getAnimationStyle(-windowWidth + offsetX - SLIDES_GAP)}> <div className="MediaViewerSlide" style={getAnimationStyle(-windowWidth + offsetX - SLIDES_GAP)}>
<MediaViewerContent <MediaViewerContent
/* eslint-disable-next-line react/jsx-props-no-spreading */ /* eslint-disable-next-line react/jsx-props-no-spreading */
{...rest} {...rest}
animationLevel={animationLevel} animationLevel={animationLevel}
isFooterHidden={isFooterHidden} isFooterHidden={isFooterHidden}
messageId={previousMessageId} mediaId={prevMediaId}
/> />
</div> </div>
)} )}
{activeMessageId && ( <div
<div className={buildClassName(
className={buildClassName( 'MediaViewerSlide',
'MediaViewerSlide', 'MediaViewerSlide--active',
isActive && 'MediaViewerSlide--active', isMouseDown && scale > 1 && 'MediaViewerSlide--moving',
isMouseDown && scale > 1 && 'MediaViewerSlide--moving', )}
)} onClick={handleToggleFooterVisibility}
onClick={handleToggleFooterVisibility} ref={activeSlideRef}
ref={activeSlideRef} style={getAnimationStyle(offsetX, offsetY, scale)}
style={getAnimationStyle(offsetX, offsetY, scale)} >
> <MediaViewerContent
<MediaViewerContent /* eslint-disable-next-line react/jsx-props-no-spreading */
/* eslint-disable-next-line react/jsx-props-no-spreading */ {...rest}
{...rest} mediaId={activeMediaId}
messageId={activeMessageId} animationLevel={animationLevel}
animationLevel={animationLevel} isActive={isActiveRef.current}
isActive={isActive && isActiveRef.current} setIsFooterHidden={setIsFooterHidden}
setIsFooterHidden={setIsFooterHidden} isFooterHidden={isFooterHidden || scale !== 1}
isFooterHidden={isFooterHidden || scale !== 1} />
/> </div>
</div> {hasNext && scale === 1 && !isResizing && (
)}
{nextMessageId && scale === 1 && !isResizing && (
<div className="MediaViewerSlide" style={getAnimationStyle(windowWidth + offsetX + SLIDES_GAP)}> <div className="MediaViewerSlide" style={getAnimationStyle(windowWidth + offsetX + SLIDES_GAP)}>
<MediaViewerContent <MediaViewerContent
/* eslint-disable-next-line react/jsx-props-no-spreading */ /* eslint-disable-next-line react/jsx-props-no-spreading */
{...rest} {...rest}
animationLevel={animationLevel} animationLevel={animationLevel}
isFooterHidden={isFooterHidden} isFooterHidden={isFooterHidden}
messageId={nextMessageId} mediaId={nextMediaId}
/> />
</div> </div>
)} )}
{previousMessageId && scale === 1 && !IS_TOUCH_ENV && ( {hasPrev && scale === 1 && !IS_TOUCH_ENV && (
<button <button
type="button" type="button"
className={`navigation prev ${isVideo && !isGif && 'inline'}`} className={`navigation prev ${isVideo && !isGif && 'inline'}`}
@ -711,7 +709,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
/> />
)} )}
{nextMessageId && scale === 1 && !IS_TOUCH_ENV && ( {hasNext && scale === 1 && !IS_TOUCH_ENV && (
<button <button
type="button" type="button"
className={`navigation next ${isVideo && !isGif && 'inline'}`} className={`navigation next ${isVideo && !isGif && 'inline'}`}

View File

@ -64,6 +64,7 @@
width: 3.25rem; width: 3.25rem;
height: 3.25rem; height: 3.25rem;
background-color: rgba(0, 0, 0, 0.5) !important; background-color: rgba(0, 0, 0, 0.5) !important;
z-index: 3;
body:not(.animation-level-0) & { body:not(.animation-level-0) & {
transition: opacity 0.3s ease !important; transition: opacity 0.3s ease !important;
} }

View File

@ -12,14 +12,14 @@ import useShowTransition from '../../hooks/useShowTransition';
import useVideoCleanup from '../../hooks/useVideoCleanup'; import useVideoCleanup from '../../hooks/useVideoCleanup';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment'; import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import safePlay from '../../util/safePlay'; import safePlay from '../../util/safePlay';
import stopEvent from '../../util/stopEvent';
import Button from '../ui/Button'; import Button from '../ui/Button';
import ProgressSpinner from '../ui/ProgressSpinner'; import ProgressSpinner from '../ui/ProgressSpinner';
import VideoPlayerControls from './VideoPlayerControls';
import './VideoPlayer.scss'; import './VideoPlayer.scss';
import VideoPlayerControls from './VideoPlayerControls';
type OwnProps = { type OwnProps = {
url?: string; url?: string;
isGif?: boolean; isGif?: boolean;
@ -33,6 +33,7 @@ type OwnProps = {
volume: number; volume: number;
isMuted: boolean; isMuted: boolean;
playbackRate: number; playbackRate: number;
isProtected?: boolean;
toggleControls: (isVisible: boolean) => void; toggleControls: (isVisible: boolean) => void;
onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void; onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
}; };
@ -54,6 +55,7 @@ const VideoPlayer: FC<OwnProps> = ({
onClose, onClose,
toggleControls, toggleControls,
areControlsVisible, areControlsVisible,
isProtected,
}) => { }) => {
const { const {
setMediaViewerVolume, setMediaViewerVolume,
@ -181,9 +183,18 @@ const VideoPlayer: FC<OwnProps> = ({
style={wrapperStyle} style={wrapperStyle}
> >
{/* eslint-disable-next-line jsx-a11y/media-has-caption */} {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
{isProtected && (
<div
onContextMenu={stopEvent}
onDoubleClick={!IS_TOUCH_ENV ? handleFullscreenChange : undefined}
onClick={!IS_SINGLE_COLUMN_LAYOUT ? togglePlayState : undefined}
className="protector"
/>
)}
<video <video
ref={videoRef} ref={videoRef}
autoPlay={IS_TOUCH_ENV} autoPlay={IS_TOUCH_ENV}
controlsList={isProtected ? 'nodownload' : undefined}
playsInline playsInline
loop={isGif} loop={isGif}
// This is to force auto playing on mobiles // This is to force auto playing on mobiles

View File

@ -9,6 +9,8 @@
font-size: 0.875rem; font-size: 0.875rem;
background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%); background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%);
transition: opacity 0.3s; transition: opacity 0.3s;
z-index: var(--z-video-player-controls);
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
@ -20,7 +22,6 @@
position: fixed; position: fixed;
padding: 2.25rem 0.5rem 0.75rem; padding: 2.25rem 0.5rem 0.75rem;
background: none; background: none;
z-index: var(--z-media-viewer);
} }
&.active { &.active {
@ -147,8 +148,16 @@
} }
} }
.playback-rate-menu .bubble { .playback-rate-menu {
min-width: 4rem; .bubble {
margin-right: 4rem; min-width: 3.5rem;
margin-right: 3.5rem;
bottom: 4.1875rem
}
&.no-fullscreen {
.bubble {
margin-right: 0.8125rem;
}
}
} }
} }

View File

@ -218,7 +218,7 @@ const VideoPlayerControls: FC<OwnProps> = ({
</div> </div>
<Menu <Menu
isOpen={isPlaybackMenuOpen} isOpen={isPlaybackMenuOpen}
className="playback-rate-menu" className={buildClassName('playback-rate-menu', !isFullscreenSupported && 'no-fullscreen')}
positionX="right" positionX="right"
positionY="bottom" positionY="bottom"
autoClose autoClose

View File

@ -0,0 +1,155 @@
import type {
ApiMessage, ApiChat, ApiUser, ApiDimensions,
} from '../../../api/types';
import { ApiMediaFormat } from '../../../api/types';
import {
getVideoAvatarMediaHash,
getChatAvatarHash,
getMessageMediaHash,
getMessagePhoto,
getMessageVideo,
getMessageWebPagePhoto,
getMessageWebPageVideo,
isMessageDocumentPhoto,
isMessageDocumentVideo,
getMessageMediaFormat,
getMessageMediaThumbDataUri,
getMessageFileName,
getMessageDocument,
getPhotoFullDimensions,
getVideoDimensions,
getMessageFileSize,
} from '../../../global/helpers';
import { useMemo } from '../../../lib/teact/teact';
import useMedia from '../../../hooks/useMedia';
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
import useBlurSync from '../../../hooks/useBlurSync';
import { MediaViewerOrigin } from '../../../types';
import { VIDEO_AVATAR_FULL_DIMENSIONS, AVATAR_FULL_DIMENSIONS } from '../../common/helpers/mediaDimensions';
type UseMediaProps = {
mediaId?: number;
message?: ApiMessage;
avatarOwner?: ApiChat | ApiUser;
origin?: MediaViewerOrigin;
lastSyncTime?: number;
delay: number | false;
};
export const useMediaProps = ({
message,
mediaId = 0,
avatarOwner,
origin,
delay,
}: UseMediaProps) => {
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 videoSize = message ? getMessageFileSize(message) : undefined;
const avatarMedia = avatarOwner?.photos?.[mediaId];
const isVideoAvatar = Boolean(avatarMedia?.isVideo);
const isVideo = Boolean(video || webPageVideo || isDocumentVideo);
const isPhoto = Boolean(!isVideo && (photo || webPagePhoto || isDocumentPhoto));
const { isGif } = video || webPageVideo || {};
const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia;
const isFromSearch = origin === MediaViewerOrigin.SearchResult;
const getMediaHash = useMemo(() => (isFull?: boolean) => {
if (avatarOwner) {
if (avatarMedia) {
if (avatarMedia.isVideo && isFull) {
return getVideoAvatarMediaHash(avatarMedia);
} else {
return `photo${avatarMedia.id}?size=c`;
}
} else {
return getChatAvatarHash(avatarOwner!, isFull ? 'big' : 'normal');
}
}
return message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview');
}, [avatarOwner, avatarMedia, message]);
const pictogramBlobUrl = useMedia(
message && (isFromSharedMedia || isFromSearch) && getMessageMediaHash(message, 'pictogram'),
undefined,
ApiMediaFormat.BlobUrl,
undefined,
delay,
);
const previewMediaHash = getMediaHash();
const previewBlobUrl = useMedia(
previewMediaHash,
undefined,
ApiMediaFormat.BlobUrl,
undefined,
delay,
);
const {
mediaData: fullMediaBlobUrl,
loadProgress,
} = useMediaWithLoadProgress(
getMediaHash(true),
undefined,
message && getMessageMediaFormat(message, 'viewerFull'),
undefined,
delay,
);
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)
: avatarOwner
? `avatar${avatarOwner!.id}.${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;
}
return {
getMediaHash,
photo,
video,
webPagePhoto,
webPageVideo,
isVideo,
isPhoto,
isGif,
isDocumentPhoto,
isDocumentVideo,
fileName,
bestImageData,
dimensions,
isFromSharedMedia,
avatarPhoto: avatarMedia,
isVideoAvatar,
localBlobUrl,
fullMediaBlobUrl,
previewBlobUrl,
pictogramBlobUrl,
loadProgress,
videoSize,
};
};

View File

@ -75,7 +75,10 @@ export default function useInnerHandlers(
const handleMediaClick = useCallback((): void => { const handleMediaClick = useCallback((): void => {
openMediaViewer({ openMediaViewer({
chatId, threadId, messageId, origin: isScheduled ? MediaViewerOrigin.ScheduledInline : MediaViewerOrigin.Inline, chatId,
threadId,
mediaId: messageId,
origin: isScheduled ? MediaViewerOrigin.ScheduledInline : MediaViewerOrigin.Inline,
}); });
}, [chatId, threadId, messageId, openMediaViewer, isScheduled]); }, [chatId, threadId, messageId, openMediaViewer, isScheduled]);
@ -87,7 +90,7 @@ export default function useInnerHandlers(
openMediaViewer({ openMediaViewer({
chatId, chatId,
threadId, threadId,
messageId: albumMessageId, mediaId: albumMessageId,
origin: isScheduled ? MediaViewerOrigin.ScheduledAlbum : MediaViewerOrigin.Album, origin: isScheduled ? MediaViewerOrigin.ScheduledAlbum : MediaViewerOrigin.Album,
}); });
}, [chatId, threadId, openMediaViewer, isScheduled]); }, [chatId, threadId, openMediaViewer, isScheduled]);

View File

@ -225,11 +225,11 @@ const Profile: FC<OwnProps & StateProps> = ({
} }
}, [loadProfilePhotos, profileId, lastSyncTime]); }, [loadProfilePhotos, profileId, lastSyncTime]);
const handleSelectMedia = useCallback((messageId: number) => { const handleSelectMedia = useCallback((mediaId: number) => {
openMediaViewer({ openMediaViewer({
chatId: profileId, chatId: profileId,
threadId: MAIN_THREAD_ID, threadId: MAIN_THREAD_ID,
messageId, mediaId,
origin: MediaViewerOrigin.SharedMedia, origin: MediaViewerOrigin.SharedMedia,
}); });
}, [profileId, openMediaViewer]); }, [profileId, openMediaViewer]);

View File

@ -2,7 +2,7 @@ import { addActionHandler } from '../../index';
addActionHandler('openMediaViewer', (global, actions, payload) => { addActionHandler('openMediaViewer', (global, actions, payload) => {
const { const {
chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin, volume, playbackRate, isMuted, chatId, threadId, mediaId, avatarOwnerId, profilePhotoIndex, origin, volume, playbackRate, isMuted,
} = payload; } = payload;
return { return {
@ -11,7 +11,7 @@ addActionHandler('openMediaViewer', (global, actions, payload) => {
...global.mediaViewer, ...global.mediaViewer,
chatId, chatId,
threadId, threadId,
messageId, mediaId,
avatarOwnerId, avatarOwnerId,
profilePhotoIndex, profilePhotoIndex,
origin, origin,

View File

@ -10,7 +10,7 @@ import { selectCurrentManagement } from './management';
export function selectIsMediaViewerOpen(global: GlobalState) { export function selectIsMediaViewerOpen(global: GlobalState) {
const { mediaViewer } = global; const { mediaViewer } = global;
return Boolean(mediaViewer.messageId || mediaViewer.avatarOwnerId); return Boolean(mediaViewer.mediaId || mediaViewer.avatarOwnerId);
} }
export function selectRightColumnContentKey(global: GlobalState) { export function selectRightColumnContentKey(global: GlobalState) {

View File

@ -399,7 +399,7 @@ export type GlobalState = {
mediaViewer: { mediaViewer: {
chatId?: string; chatId?: string;
threadId?: number; threadId?: number;
messageId?: number; mediaId?: number;
avatarOwnerId?: string; avatarOwnerId?: string;
profilePhotoIndex?: number; profilePhotoIndex?: number;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
@ -750,7 +750,7 @@ export interface ActionPayloads {
openMediaViewer: { openMediaViewer: {
chatId?: string; chatId?: string;
threadId?: number; threadId?: number;
messageId?: number; mediaId?: number;
avatarOwnerId?: string; avatarOwnerId?: string;
profilePhotoIndex?: number; profilePhotoIndex?: number;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;

View File

@ -211,6 +211,7 @@ $color-message-reaction-own-hover: #b5e0a4;
--z-header-menu-backdrop: 980; --z-header-menu-backdrop: 980;
--z-modal: 1510; --z-modal: 1510;
--z-media-viewer: 1500; --z-media-viewer: 1500;
--z-video-player-controls: 3;
--z-drop-area: 55; --z-drop-area: 55;
--z-animation-fade: 50; --z-animation-fade: 50;
--z-menu-bubble: 21; --z-menu-bubble: 21;

View File

@ -265,6 +265,8 @@ div[role="button"] {
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 2; z-index: 2;
user-select: none;
-webkit-touch-callout: none;
} }
.for-ios-autocapitalization-fix { .for-ios-autocapitalization-fix {