Media Viewer: Support switching avatars (#1929)
This commit is contained in:
parent
06fe3a2640
commit
a55b410e6a
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|||||||
@ -176,6 +176,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-protected {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ghost {
|
.ghost {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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'}`}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
155
src/components/mediaViewer/hooks/useMediaProps.ts
Normal file
155
src/components/mediaViewer/hooks/useMediaProps.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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]);
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user