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 {
|
||||
loadFullChat,
|
||||
openMediaViewer,
|
||||
loadProfilePhotos,
|
||||
} = getActions();
|
||||
|
||||
const isSuperGroup = chat && isChatSuperGroup(chat);
|
||||
const { id: chatId, isMin, isRestricted } = chat || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (chatId && !isMin && withFullInfo && lastSyncTime) {
|
||||
loadFullChat({ chatId });
|
||||
if (chatId && !isMin && lastSyncTime) {
|
||||
if (withFullInfo) loadFullChat({ chatId });
|
||||
if (withMediaViewer) loadProfilePhotos({ profileId: chatId });
|
||||
}
|
||||
}, [chatId, isMin, lastSyncTime, withFullInfo, loadFullChat, isSuperGroup]);
|
||||
}, [chatId, isMin, lastSyncTime, withFullInfo, loadFullChat, loadProfilePhotos, isSuperGroup, withMediaViewer]);
|
||||
|
||||
const handleAvatarViewerOpen = useCallback((e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {
|
||||
if (chat && hasMedia) {
|
||||
e.stopPropagation();
|
||||
openMediaViewer({
|
||||
avatarOwnerId: chat.id,
|
||||
mediaId: 0,
|
||||
origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar,
|
||||
});
|
||||
}
|
||||
|
||||
@ -67,22 +67,25 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
const {
|
||||
loadFullUser,
|
||||
openMediaViewer,
|
||||
loadProfilePhotos,
|
||||
} = getActions();
|
||||
|
||||
const { id: userId } = user || {};
|
||||
const fullName = getUserFullName(user);
|
||||
|
||||
useEffect(() => {
|
||||
if (withFullInfo && lastSyncTime && userId) {
|
||||
loadFullUser({ userId });
|
||||
if (userId && lastSyncTime) {
|
||||
if (withFullInfo) loadFullUser({ userId });
|
||||
if (withMediaViewer) loadProfilePhotos({ profileId: userId });
|
||||
}
|
||||
}, [userId, loadFullUser, lastSyncTime, withFullInfo]);
|
||||
}, [userId, loadFullUser, loadProfilePhotos, lastSyncTime, withFullInfo, withMediaViewer]);
|
||||
|
||||
const handleAvatarViewerOpen = useCallback((e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {
|
||||
if (user && hasMedia) {
|
||||
e.stopPropagation();
|
||||
openMediaViewer({
|
||||
avatarOwnerId: user.id,
|
||||
mediaId: 0,
|
||||
origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar,
|
||||
});
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import type { GlobalState } from '../../global/types';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import { selectChat, selectUser, selectUserStatus } from '../../global/selectors';
|
||||
import {
|
||||
getUserFullName, getUserStatus, isChatChannel, isUserOnline,
|
||||
@ -18,6 +19,7 @@ import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import usePhotosPreload from './hooks/usePhotosPreload';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
|
||||
import VerifiedIcon from './VerifiedIcon';
|
||||
import ProfilePhoto from './ProfilePhoto';
|
||||
@ -40,6 +42,8 @@ type StateProps =
|
||||
isSavedMessages?: boolean;
|
||||
animationLevel: 0 | 1 | 2;
|
||||
serverTimeOffset: number;
|
||||
mediaId?: number;
|
||||
avatarOwnerId?: string;
|
||||
}
|
||||
& Pick<GlobalState, 'connectionState'>;
|
||||
|
||||
@ -52,6 +56,8 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
connectionState,
|
||||
animationLevel,
|
||||
serverTimeOffset,
|
||||
mediaId,
|
||||
avatarOwnerId,
|
||||
}) => {
|
||||
const {
|
||||
loadFullUser,
|
||||
@ -64,15 +70,26 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
const { id: userId } = user || {};
|
||||
const { id: chatId } = chat || {};
|
||||
const fullName = user ? getUserFullName(user) : (chat ? chat.title : '');
|
||||
const photos = user?.photos || chat?.photos || [];
|
||||
const slideAnimation = animationLevel >= 1
|
||||
? (lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized')
|
||||
const photos = user?.photos || chat?.photos || MEMO_EMPTY_ARRAY;
|
||||
const prevMediaId = usePrevious(mediaId);
|
||||
const prevAvatarOwnerId = usePrevious(avatarOwnerId);
|
||||
const [hasSlideAnimation, setHasSlideAnimation] = useState(true);
|
||||
const slideAnimation = hasSlideAnimation
|
||||
? animationLevel >= 1 ? (lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized') : 'none'
|
||||
: 'none';
|
||||
|
||||
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
||||
const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0;
|
||||
const isLast = isSavedMessages || photos.length <= 1 || currentPhotoIndex === photos.length - 1;
|
||||
|
||||
// Set the current avatar photo to the last selected photo in Media Viewer after it is closed
|
||||
useEffect(() => {
|
||||
if (prevAvatarOwnerId && prevMediaId !== undefined && mediaId === undefined) {
|
||||
setHasSlideAnimation(false);
|
||||
setCurrentPhotoIndex(prevMediaId);
|
||||
}
|
||||
}, [mediaId, prevMediaId, prevAvatarOwnerId]);
|
||||
|
||||
// Deleting the last profile photo may result in an error
|
||||
useEffect(() => {
|
||||
if (currentPhotoIndex > photos.length) {
|
||||
@ -91,7 +108,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
const handleProfilePhotoClick = useCallback(() => {
|
||||
openMediaViewer({
|
||||
avatarOwnerId: userId || chatId,
|
||||
profilePhotoIndex: currentPhotoIndex,
|
||||
mediaId: currentPhotoIndex,
|
||||
origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar,
|
||||
});
|
||||
}, [openMediaViewer, userId, chatId, currentPhotoIndex, forceShowSelf]);
|
||||
@ -106,7 +123,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
if (isFirst) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHasSlideAnimation(true);
|
||||
setCurrentPhotoIndex(currentPhotoIndex - 1);
|
||||
}, [currentPhotoIndex, isFirst]);
|
||||
|
||||
@ -114,7 +131,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
if (isLast) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHasSlideAnimation(true);
|
||||
setCurrentPhotoIndex(currentPhotoIndex + 1);
|
||||
}, [currentPhotoIndex, isLast]);
|
||||
|
||||
@ -251,6 +268,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const chat = selectChat(global, userId);
|
||||
const isSavedMessages = !forceShowSelf && user && user.isSelf;
|
||||
const { animationLevel } = global.settings.byKey;
|
||||
const { mediaId, avatarOwnerId } = global.mediaViewer;
|
||||
|
||||
return {
|
||||
connectionState,
|
||||
@ -260,6 +278,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
isSavedMessages,
|
||||
animationLevel,
|
||||
serverTimeOffset,
|
||||
mediaId,
|
||||
avatarOwnerId,
|
||||
};
|
||||
},
|
||||
)(ProfileInfo));
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
} from '../../global/helpers';
|
||||
import renderText from './helpers/renderText';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import safePlay from '../../util/safePlay';
|
||||
import { getFirstLetters } from '../../util/textFormat';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useLang from '../../hooks/useLang';
|
||||
@ -80,7 +81,7 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
videoRef.current.pause();
|
||||
videoRef.current.currentTime = 0;
|
||||
} else {
|
||||
videoRef.current.play();
|
||||
safePlay(videoRef.current);
|
||||
}
|
||||
}, [notActive]);
|
||||
|
||||
|
||||
@ -79,10 +79,10 @@ const MediaResults: FC<OwnProps & StateProps> = ({
|
||||
}).filter(Boolean);
|
||||
}, [globalMessagesByChatId, foundIds]);
|
||||
|
||||
const handleSelectMedia = useCallback((messageId: number, chatId: string) => {
|
||||
const handleSelectMedia = useCallback((id: number, chatId: string) => {
|
||||
openMediaViewer({
|
||||
chatId,
|
||||
messageId,
|
||||
mediaId: id,
|
||||
origin: MediaViewerOrigin.SearchResult,
|
||||
});
|
||||
}, [openMediaViewer]);
|
||||
|
||||
@ -176,6 +176,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-protected {
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ghost {
|
||||
|
||||
@ -4,28 +4,13 @@ import React, {
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiDimensions, ApiMessage, ApiUser,
|
||||
ApiChat, ApiMessage, ApiUser,
|
||||
} from '../../api/types';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
import {
|
||||
getChatAvatarHash,
|
||||
getChatMediaMessageIds,
|
||||
getMessageDocument,
|
||||
getMessageFileName,
|
||||
getMessageMediaFormat,
|
||||
getMessageMediaHash,
|
||||
getMessageMediaThumbDataUri,
|
||||
getMessagePhoto,
|
||||
getMessageVideo,
|
||||
getMessageWebPagePhoto,
|
||||
getMessageWebPageVideo,
|
||||
getPhotoFullDimensions, getVideoAvatarMediaHash,
|
||||
getVideoDimensions,
|
||||
isMessageDocumentPhoto,
|
||||
isMessageDocumentVideo,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
@ -43,22 +28,18 @@ import { stopCurrentAudio } from '../../util/audioPlayer';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
import {
|
||||
AVATAR_FULL_DIMENSIONS, MEDIA_VIEWER_MEDIA_QUERY, VIDEO_AVATAR_FULL_DIMENSIONS,
|
||||
} from '../common/helpers/mediaDimensions';
|
||||
import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions';
|
||||
import windowSize from '../../util/windowSize';
|
||||
import { animateClosing, animateOpening } from './helpers/ghostAnimation';
|
||||
import { renderMessageText } from '../common/helpers/renderMessageText';
|
||||
|
||||
import useBlurSync from '../../hooks/useBlurSync';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { useMediaProps } from './hooks/useMediaProps';
|
||||
|
||||
import ReportModal from '../common/ReportModal';
|
||||
import Button from '../ui/Button';
|
||||
@ -73,12 +54,11 @@ import './MediaViewer.scss';
|
||||
type StateProps = {
|
||||
chatId?: string;
|
||||
threadId?: number;
|
||||
messageId?: number;
|
||||
mediaId?: number;
|
||||
senderId?: string;
|
||||
isChatWithSelf?: boolean;
|
||||
origin?: MediaViewerOrigin;
|
||||
avatarOwner?: ApiChat | ApiUser;
|
||||
profilePhotoIndex?: number;
|
||||
message?: ApiMessage;
|
||||
chatMessages?: Record<number, ApiMessage>;
|
||||
collectionIds?: number[];
|
||||
@ -90,12 +70,11 @@ const ANIMATION_DURATION = 350;
|
||||
const MediaViewer: FC<StateProps> = ({
|
||||
chatId,
|
||||
threadId,
|
||||
messageId,
|
||||
mediaId,
|
||||
senderId,
|
||||
isChatWithSelf,
|
||||
origin,
|
||||
avatarOwner,
|
||||
profilePhotoIndex,
|
||||
message,
|
||||
chatMessages,
|
||||
collectionIds,
|
||||
@ -109,40 +88,11 @@ const MediaViewer: FC<StateProps> = ({
|
||||
toggleChatInfo,
|
||||
} = getActions();
|
||||
|
||||
const isOpen = Boolean(avatarOwner || messageId);
|
||||
|
||||
const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia;
|
||||
const isFromSearch = origin === MediaViewerOrigin.SearchResult;
|
||||
|
||||
/* Content */
|
||||
const photo = message ? getMessagePhoto(message) : undefined;
|
||||
const video = message ? getMessageVideo(message) : undefined;
|
||||
const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined;
|
||||
const webPageVideo = message ? getMessageWebPageVideo(message) : undefined;
|
||||
const isDocumentPhoto = message ? isMessageDocumentPhoto(message) : false;
|
||||
const isDocumentVideo = message ? isMessageDocumentVideo(message) : false;
|
||||
const isVideo = Boolean(video || webPageVideo || isDocumentVideo);
|
||||
const { isGif } = video || webPageVideo || {};
|
||||
const isPhoto = Boolean(!isVideo && (photo || webPagePhoto || isDocumentPhoto));
|
||||
const isAvatar = Boolean(avatarOwner);
|
||||
|
||||
/* Navigation */
|
||||
const singleMessageId = webPagePhoto || webPageVideo ? messageId : undefined;
|
||||
|
||||
const messageIds = useMemo(() => {
|
||||
return singleMessageId
|
||||
? [singleMessageId]
|
||||
: getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia);
|
||||
}, [singleMessageId, chatMessages, collectionIds, isFromSharedMedia]);
|
||||
|
||||
const selectedMediaMessageIndex = messageId ? messageIds.indexOf(messageId) : -1;
|
||||
const isOpen = Boolean(avatarOwner || mediaId);
|
||||
|
||||
/* Animation */
|
||||
const animationKey = useRef<number>();
|
||||
const prevSenderId = usePrevious<string | undefined>(senderId);
|
||||
if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) {
|
||||
animationKey.current = selectedMediaMessageIndex;
|
||||
}
|
||||
const headerAnimation = animationLevel === 2 ? 'slide-fade' : 'none';
|
||||
const isGhostAnimation = animationLevel === 2;
|
||||
|
||||
@ -150,80 +100,46 @@ const MediaViewer: FC<StateProps> = ({
|
||||
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
|
||||
const [zoomLevelChange, setZoomLevelChange] = useState<number>(1);
|
||||
|
||||
/* Media data */
|
||||
function getMediaHash(isFull?: boolean) {
|
||||
if (isAvatar && profilePhotoIndex !== undefined) {
|
||||
const { photos } = avatarOwner!;
|
||||
const avatarPhoto = photos && photos[profilePhotoIndex];
|
||||
return avatarPhoto
|
||||
// Video for avatar should be used only for full size
|
||||
? (avatarPhoto.isVideo && isFull ? getVideoAvatarMediaHash(avatarPhoto) : `photo${avatarPhoto.id}?size=c`)
|
||||
: getChatAvatarHash(avatarOwner!, isFull ? 'big' : 'normal');
|
||||
const {
|
||||
webPagePhoto,
|
||||
webPageVideo,
|
||||
isVideo,
|
||||
isPhoto,
|
||||
bestImageData,
|
||||
dimensions,
|
||||
isGif,
|
||||
isFromSharedMedia,
|
||||
avatarPhoto,
|
||||
fileName,
|
||||
fullMediaBlobUrl,
|
||||
previewBlobUrl,
|
||||
} = useMediaProps({
|
||||
message, avatarOwner, mediaId, delay: isGhostAnimation && ANIMATION_DURATION,
|
||||
});
|
||||
|
||||
const canReport = !!avatarPhoto && !isChatWithSelf;
|
||||
|
||||
/* Navigation */
|
||||
const singleMediaId = webPagePhoto || webPageVideo ? mediaId : undefined;
|
||||
|
||||
const mediaIds = useMemo(() => {
|
||||
if (singleMediaId) {
|
||||
return [singleMediaId];
|
||||
} else if (avatarOwner) {
|
||||
return avatarOwner.photos?.map((p, i) => i) || [];
|
||||
} else {
|
||||
return getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia);
|
||||
}
|
||||
}, [singleMediaId, avatarOwner, chatMessages, collectionIds, isFromSharedMedia]);
|
||||
|
||||
return message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview');
|
||||
}
|
||||
const selectedMediaIndex = mediaId ? mediaIds.indexOf(mediaId) : -1;
|
||||
|
||||
const pictogramBlobUrl = useMedia(
|
||||
message && (isFromSharedMedia || isFromSearch) && getMessageMediaHash(message, 'pictogram'),
|
||||
undefined,
|
||||
ApiMediaFormat.BlobUrl,
|
||||
undefined,
|
||||
isGhostAnimation && ANIMATION_DURATION,
|
||||
);
|
||||
const previewMediaHash = getMediaHash();
|
||||
const previewBlobUrl = useMedia(
|
||||
previewMediaHash,
|
||||
undefined,
|
||||
ApiMediaFormat.BlobUrl,
|
||||
undefined,
|
||||
isGhostAnimation && ANIMATION_DURATION,
|
||||
);
|
||||
const { mediaData: fullMediaBlobUrl } = useMediaWithLoadProgress(
|
||||
getMediaHash(true),
|
||||
undefined,
|
||||
message && getMessageMediaFormat(message, 'viewerFull'),
|
||||
undefined,
|
||||
isGhostAnimation && ANIMATION_DURATION,
|
||||
);
|
||||
const avatarPhoto = avatarOwner?.photos?.[profilePhotoIndex!];
|
||||
const isVideoAvatar = Boolean(isAvatar && avatarPhoto?.isVideo);
|
||||
const canReport = !!avatarPhoto && profilePhotoIndex! > 0 && !isChatWithSelf;
|
||||
|
||||
const localBlobUrl = (photo || video) ? (photo || video)!.blobUrl : undefined;
|
||||
let bestImageData = (!isVideo && (localBlobUrl || fullMediaBlobUrl)) || previewBlobUrl || pictogramBlobUrl;
|
||||
const thumbDataUri = useBlurSync(!bestImageData && message && getMessageMediaThumbDataUri(message));
|
||||
if (!bestImageData && origin !== MediaViewerOrigin.SearchResult) {
|
||||
bestImageData = thumbDataUri;
|
||||
}
|
||||
if (isVideoAvatar && previewBlobUrl) {
|
||||
bestImageData = previewBlobUrl;
|
||||
}
|
||||
|
||||
const fileName = message
|
||||
? getMessageFileName(message)
|
||||
: isAvatar
|
||||
? `avatar${avatarOwner!.id}-${profilePhotoIndex}.${avatarOwner?.hasVideoAvatar ? 'mp4' : 'jpg'}`
|
||||
: undefined;
|
||||
|
||||
let dimensions!: ApiDimensions;
|
||||
if (message) {
|
||||
if (isDocumentPhoto || isDocumentVideo) {
|
||||
dimensions = getMessageDocument(message)!.mediaSize!;
|
||||
} else if (photo || webPagePhoto) {
|
||||
dimensions = getPhotoFullDimensions((photo || webPagePhoto)!)!;
|
||||
} else if (video || webPageVideo) {
|
||||
dimensions = getVideoDimensions((video || webPageVideo)!)!;
|
||||
}
|
||||
} else {
|
||||
dimensions = isVideoAvatar ? VIDEO_AVATAR_FULL_DIMENSIONS : AVATAR_FULL_DIMENSIONS;
|
||||
if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) {
|
||||
animationKey.current = selectedMediaIndex;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IS_SINGLE_COLUMN_LAYOUT) return;
|
||||
document.body.classList.toggle('is-media-viewer-open', isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
@ -277,28 +193,31 @@ const MediaViewer: FC<StateProps> = ({
|
||||
if (IS_SINGLE_COLUMN_LAYOUT) {
|
||||
setTimeout(() => {
|
||||
toggleChatInfo(false, { forceSyncOnIOs: true });
|
||||
focusMessage({ chatId, threadId, messageId });
|
||||
focusMessage({ chatId, threadId, mediaId });
|
||||
}, ANIMATION_DURATION);
|
||||
} else {
|
||||
focusMessage({ chatId, threadId, messageId });
|
||||
focusMessage({ chatId, threadId, mediaId });
|
||||
}
|
||||
}, [close, chatId, threadId, focusMessage, toggleChatInfo, messageId]);
|
||||
}, [close, chatId, threadId, focusMessage, toggleChatInfo, mediaId]);
|
||||
|
||||
const handleForward = useCallback(() => {
|
||||
openForwardMenu({
|
||||
fromChatId: chatId,
|
||||
messageIds: [messageId],
|
||||
messageIds: [mediaId],
|
||||
});
|
||||
}, [openForwardMenu, chatId, messageId]);
|
||||
}, [openForwardMenu, chatId, mediaId]);
|
||||
|
||||
const selectMessage = useCallback((id?: number) => openMediaViewer({
|
||||
chatId,
|
||||
threadId,
|
||||
messageId: id,
|
||||
origin,
|
||||
}, {
|
||||
forceOnHeavyAnimation: true,
|
||||
}), [chatId, openMediaViewer, origin, threadId]);
|
||||
const selectMedia = useCallback((id?: number) => {
|
||||
openMediaViewer({
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId: id,
|
||||
avatarOwnerId: avatarOwner?.id,
|
||||
origin,
|
||||
}, {
|
||||
forceOnHeavyAnimation: true,
|
||||
});
|
||||
}, [avatarOwner?.id, chatId, openMediaViewer, origin, threadId]);
|
||||
|
||||
useEffect(() => (isOpen ? captureEscKeyListener(() => {
|
||||
close();
|
||||
@ -323,14 +242,14 @@ const MediaViewer: FC<StateProps> = ({
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const getMessageId = useCallback((fromId?: number, direction?: number): number | undefined => {
|
||||
if (!fromId) return undefined;
|
||||
const index = messageIds.indexOf(fromId);
|
||||
if ((direction === -1 && index > 0) || (direction === 1 && index < messageIds.length - 1)) {
|
||||
return messageIds[index + direction];
|
||||
const getMediaId = useCallback((fromId?: number, direction?: number): number | undefined => {
|
||||
if (fromId === undefined) return undefined;
|
||||
const index = mediaIds.indexOf(fromId);
|
||||
if ((direction === -1 && index > 0) || (direction === 1 && index < mediaIds.length - 1)) {
|
||||
return mediaIds[index + direction];
|
||||
}
|
||||
return undefined;
|
||||
}, [messageIds]);
|
||||
}, [mediaIds]);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -340,17 +259,17 @@ const MediaViewer: FC<StateProps> = ({
|
||||
});
|
||||
|
||||
function renderSenderInfo() {
|
||||
return isAvatar ? (
|
||||
return avatarOwner ? (
|
||||
<SenderInfo
|
||||
key={avatarOwner!.id}
|
||||
chatId={avatarOwner!.id}
|
||||
key={avatarOwner.id}
|
||||
chatId={avatarOwner.id}
|
||||
isAvatar
|
||||
/>
|
||||
) : (
|
||||
<SenderInfo
|
||||
key={messageId}
|
||||
key={mediaId}
|
||||
chatId={chatId}
|
||||
messageId={messageId}
|
||||
messageId={mediaId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -384,7 +303,7 @@ const MediaViewer: FC<StateProps> = ({
|
||||
onForward={handleForward}
|
||||
zoomLevelChange={zoomLevelChange}
|
||||
setZoomLevelChange={setZoomLevelChange}
|
||||
isAvatar={isAvatar}
|
||||
isAvatar={Boolean(avatarOwner)}
|
||||
/>
|
||||
<ReportModal
|
||||
isOpen={isReportModalOpen}
|
||||
@ -395,23 +314,21 @@ const MediaViewer: FC<StateProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<MediaViewerSlides
|
||||
messageId={messageId}
|
||||
getMessageId={getMessageId}
|
||||
mediaId={mediaId}
|
||||
getMediaId={getMediaId}
|
||||
chatId={chatId}
|
||||
isPhoto={isPhoto}
|
||||
isGif={isGif}
|
||||
threadId={threadId}
|
||||
avatarOwnerId={avatarOwner && avatarOwner.id}
|
||||
profilePhotoIndex={profilePhotoIndex}
|
||||
avatarOwnerId={avatarOwner?.id}
|
||||
origin={origin}
|
||||
isOpen={isOpen}
|
||||
hasFooter={hasFooter}
|
||||
zoomLevelChange={zoomLevelChange}
|
||||
isActive
|
||||
isVideo={isVideo}
|
||||
animationLevel={animationLevel}
|
||||
onClose={close}
|
||||
selectMessage={selectMessage}
|
||||
selectMedia={selectMedia}
|
||||
onFooterClick={handleFooterClick}
|
||||
/>
|
||||
</ShowTransition>
|
||||
@ -423,9 +340,8 @@ export default memo(withGlobal(
|
||||
const {
|
||||
chatId,
|
||||
threadId,
|
||||
messageId,
|
||||
mediaId,
|
||||
avatarOwnerId,
|
||||
profilePhotoIndex,
|
||||
origin,
|
||||
} = global.mediaViewer;
|
||||
const {
|
||||
@ -435,18 +351,18 @@ export default memo(withGlobal(
|
||||
let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId);
|
||||
|
||||
if (origin === MediaViewerOrigin.SearchResult) {
|
||||
if (!(chatId && messageId)) {
|
||||
if (!(chatId && mediaId)) {
|
||||
return { animationLevel };
|
||||
}
|
||||
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
const message = selectChatMessage(global, chatId, mediaId);
|
||||
if (!message) {
|
||||
return { animationLevel };
|
||||
}
|
||||
|
||||
return {
|
||||
chatId,
|
||||
messageId,
|
||||
mediaId,
|
||||
senderId: message.senderId,
|
||||
isChatWithSelf,
|
||||
origin,
|
||||
@ -460,25 +376,24 @@ export default memo(withGlobal(
|
||||
isChatWithSelf = selectIsChatWithSelf(global, avatarOwnerId);
|
||||
|
||||
return {
|
||||
messageId: -1,
|
||||
mediaId,
|
||||
senderId: avatarOwnerId,
|
||||
avatarOwner: sender,
|
||||
isChatWithSelf,
|
||||
profilePhotoIndex: profilePhotoIndex || 0,
|
||||
animationLevel,
|
||||
origin,
|
||||
};
|
||||
}
|
||||
|
||||
if (!(chatId && threadId && messageId)) {
|
||||
if (!(chatId && threadId && mediaId)) {
|
||||
return { animationLevel };
|
||||
}
|
||||
|
||||
let message: ApiMessage | undefined;
|
||||
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
|
||||
message = selectScheduledMessage(global, chatId, messageId);
|
||||
message = selectScheduledMessage(global, chatId, mediaId);
|
||||
} else {
|
||||
message = selectChatMessage(global, chatId, messageId);
|
||||
message = selectChatMessage(global, chatId, mediaId);
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
@ -505,7 +420,7 @@ export default memo(withGlobal(
|
||||
return {
|
||||
chatId,
|
||||
threadId,
|
||||
messageId,
|
||||
mediaId,
|
||||
senderId: message.senderId,
|
||||
isChatWithSelf,
|
||||
origin,
|
||||
|
||||
@ -5,37 +5,17 @@ import { withGlobal } from '../../global';
|
||||
import type {
|
||||
ApiChat, ApiDimensions, ApiMessage, ApiUser,
|
||||
} from '../../api/types';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
|
||||
import useBlurSync from '../../hooks/useBlurSync';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||
import {
|
||||
getChatAvatarHash,
|
||||
getMessageDocument,
|
||||
getMessageFileSize,
|
||||
getMessageMediaFormat,
|
||||
getMessageMediaHash,
|
||||
getMessageMediaThumbDataUri,
|
||||
getMessagePhoto,
|
||||
getMessageVideo,
|
||||
getMessageWebPagePhoto,
|
||||
getMessageWebPageVideo,
|
||||
getPhotoFullDimensions, getVideoAvatarMediaHash,
|
||||
getVideoDimensions,
|
||||
isMessageDocumentPhoto,
|
||||
isMessageDocumentVideo,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectUser,
|
||||
} from '../../global/selectors';
|
||||
import {
|
||||
AVATAR_FULL_DIMENSIONS, calculateMediaViewerDimensions, VIDEO_AVATAR_FULL_DIMENSIONS,
|
||||
} from '../common/helpers/mediaDimensions';
|
||||
import { calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions';
|
||||
import { renderMessageText } from '../common/helpers/renderMessageText';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { useMediaProps } from './hooks/useMediaProps';
|
||||
|
||||
import Spinner from '../ui/Spinner';
|
||||
import MediaViewerFooter from './MediaViewerFooter';
|
||||
@ -44,11 +24,10 @@ import VideoPlayer from './VideoPlayer';
|
||||
import './MediaViewerContent.scss';
|
||||
|
||||
type OwnProps = {
|
||||
messageId?: number;
|
||||
mediaId?: number;
|
||||
chatId?: string;
|
||||
threadId?: number;
|
||||
avatarOwnerId?: string;
|
||||
profilePhotoIndex?: number;
|
||||
origin?: MediaViewerOrigin;
|
||||
isActive?: boolean;
|
||||
animationLevel: 0 | 1 | 2;
|
||||
@ -60,11 +39,10 @@ type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
chatId?: string;
|
||||
messageId?: number;
|
||||
mediaId?: number;
|
||||
senderId?: string;
|
||||
threadId?: number;
|
||||
avatarOwner?: ApiChat | ApiUser;
|
||||
profilePhotoIndex?: number;
|
||||
message?: ApiMessage;
|
||||
origin?: MediaViewerOrigin;
|
||||
isProtected?: boolean;
|
||||
@ -77,12 +55,11 @@ const ANIMATION_DURATION = 350;
|
||||
|
||||
const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
const {
|
||||
messageId,
|
||||
mediaId,
|
||||
isActive,
|
||||
avatarOwner,
|
||||
chatId,
|
||||
message,
|
||||
profilePhotoIndex,
|
||||
origin,
|
||||
animationLevel,
|
||||
isFooterHidden,
|
||||
@ -94,63 +71,27 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
onFooterClick,
|
||||
setIsFooterHidden,
|
||||
} = props;
|
||||
/* Content */
|
||||
const photo = message ? getMessagePhoto(message) : undefined;
|
||||
const video = message ? getMessageVideo(message) : undefined;
|
||||
const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined;
|
||||
const webPageVideo = message ? getMessageWebPageVideo(message) : undefined;
|
||||
const isDocumentPhoto = message ? isMessageDocumentPhoto(message) : false;
|
||||
const isDocumentVideo = message ? isMessageDocumentVideo(message) : false;
|
||||
const isVideo = Boolean(video || webPageVideo || isDocumentVideo);
|
||||
const isPhoto = Boolean(!isVideo && (photo || webPagePhoto || isDocumentPhoto));
|
||||
const { isGif } = video || webPageVideo || {};
|
||||
|
||||
const isOpen = Boolean(avatarOwner || messageId);
|
||||
const isAvatar = Boolean(avatarOwner);
|
||||
const isVideoAvatar = isAvatar && avatarOwner.hasVideoAvatar;
|
||||
|
||||
const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia;
|
||||
const isFromSearch = origin === MediaViewerOrigin.SearchResult;
|
||||
|
||||
const isGhostAnimation = animationLevel === 2;
|
||||
|
||||
/* Media data */
|
||||
function getMediaHash(isFull?: boolean) {
|
||||
if (isAvatar && profilePhotoIndex !== undefined) {
|
||||
const { photos, hasVideoAvatar } = avatarOwner!;
|
||||
const avatarPhoto = photos && photos[profilePhotoIndex];
|
||||
return avatarPhoto ? (hasVideoAvatar ? getVideoAvatarMediaHash(avatarPhoto) : `photo${avatarPhoto.id}?size=c`)
|
||||
: getChatAvatarHash(avatarOwner!, isFull ? 'big' : 'normal');
|
||||
}
|
||||
|
||||
return message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview');
|
||||
}
|
||||
|
||||
const pictogramBlobUrl = useMedia(
|
||||
message && (isFromSharedMedia || isFromSearch) && getMessageMediaHash(message, 'pictogram'),
|
||||
undefined,
|
||||
ApiMediaFormat.BlobUrl,
|
||||
undefined,
|
||||
isGhostAnimation && ANIMATION_DURATION,
|
||||
);
|
||||
const previewMediaHash = getMediaHash();
|
||||
const previewBlobUrl = useMedia(
|
||||
previewMediaHash,
|
||||
undefined,
|
||||
ApiMediaFormat.BlobUrl,
|
||||
undefined,
|
||||
isGhostAnimation && ANIMATION_DURATION,
|
||||
);
|
||||
const {
|
||||
mediaData: fullMediaBlobUrl,
|
||||
isVideo,
|
||||
isPhoto,
|
||||
bestImageData,
|
||||
dimensions,
|
||||
isGif,
|
||||
isVideoAvatar,
|
||||
localBlobUrl,
|
||||
fullMediaBlobUrl,
|
||||
previewBlobUrl,
|
||||
pictogramBlobUrl,
|
||||
videoSize,
|
||||
loadProgress,
|
||||
} = useMediaWithLoadProgress(
|
||||
getMediaHash(true),
|
||||
undefined,
|
||||
message && getMessageMediaFormat(message, 'viewerFull'),
|
||||
undefined,
|
||||
isGhostAnimation && ANIMATION_DURATION,
|
||||
);
|
||||
} = useMediaProps({
|
||||
message, avatarOwner, mediaId, origin, delay: isGhostAnimation && ANIMATION_DURATION,
|
||||
});
|
||||
|
||||
const isOpen = Boolean(avatarOwner || mediaId);
|
||||
|
||||
const toggleControls = useCallback((isVisible) => {
|
||||
setIsFooterHidden?.(!isVisible);
|
||||
@ -164,29 +105,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
toggleControls(false);
|
||||
}, [toggleControls]);
|
||||
|
||||
const localBlobUrl = (photo || video) ? (photo || video)!.blobUrl : undefined;
|
||||
let bestImageData = (!isVideo && (localBlobUrl || fullMediaBlobUrl)) || previewBlobUrl || pictogramBlobUrl;
|
||||
const thumbDataUri = useBlurSync(!bestImageData && message && getMessageMediaThumbDataUri(message));
|
||||
if (!bestImageData && origin !== MediaViewerOrigin.SearchResult) {
|
||||
bestImageData = thumbDataUri;
|
||||
}
|
||||
|
||||
const videoSize = message ? getMessageFileSize(message) : undefined;
|
||||
|
||||
let dimensions!: ApiDimensions;
|
||||
if (message) {
|
||||
if (isDocumentPhoto || isDocumentVideo) {
|
||||
dimensions = getMessageDocument(message)!.mediaSize!;
|
||||
} else if (photo || webPagePhoto) {
|
||||
dimensions = getPhotoFullDimensions((photo || webPagePhoto)!)!;
|
||||
} else if (video || webPageVideo) {
|
||||
dimensions = getVideoDimensions((video || webPageVideo)!)!;
|
||||
}
|
||||
} else {
|
||||
dimensions = isVideoAvatar ? VIDEO_AVATAR_FULL_DIMENSIONS : AVATAR_FULL_DIMENSIONS;
|
||||
}
|
||||
|
||||
if (isAvatar) {
|
||||
if (avatarOwner) {
|
||||
if (!isVideoAvatar) {
|
||||
return (
|
||||
<div key={chatId} className="MediaViewerContent">
|
||||
@ -194,6 +113,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
fullMediaBlobUrl || previewBlobUrl,
|
||||
calculateMediaViewerDimensions(dimensions, false),
|
||||
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
|
||||
isProtected,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@ -201,7 +121,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
return (
|
||||
<div key={chatId} className="MediaViewerContent">
|
||||
<VideoPlayer
|
||||
key={messageId}
|
||||
key={mediaId}
|
||||
url={localBlobUrl || fullMediaBlobUrl}
|
||||
isGif
|
||||
posterData={bestImageData}
|
||||
@ -211,6 +131,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
isMediaViewerOpen={isOpen && isActive}
|
||||
areControlsVisible={!isFooterHidden}
|
||||
toggleControls={toggleControls}
|
||||
isProtected={isProtected}
|
||||
noPlay={!isActive}
|
||||
onClose={onClose}
|
||||
isMuted
|
||||
@ -228,23 +149,24 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`MediaViewerContent ${hasFooter ? 'has-footer' : ''}`}
|
||||
className={buildClassName('MediaViewerContent', hasFooter && 'has-footer')}
|
||||
onMouseMove={!isGif && !IS_TOUCH_ENV ? handleMouseMove : undefined}
|
||||
onMouseOut={!isGif && !IS_TOUCH_ENV ? handleMouseOut : undefined}
|
||||
>
|
||||
{isProtected && <div onContextMenu={stopEvent} className="protector" />}
|
||||
{isPhoto && renderPhoto(
|
||||
localBlobUrl || fullMediaBlobUrl || previewBlobUrl || pictogramBlobUrl,
|
||||
message && calculateMediaViewerDimensions(dimensions!, hasFooter),
|
||||
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
|
||||
isProtected,
|
||||
)}
|
||||
{isVideo && (!isActive ? renderVideoPreview(
|
||||
bestImageData,
|
||||
message && calculateMediaViewerDimensions(dimensions!, hasFooter, true),
|
||||
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
|
||||
isProtected,
|
||||
) : (
|
||||
<VideoPlayer
|
||||
key={messageId}
|
||||
key={mediaId}
|
||||
url={localBlobUrl || fullMediaBlobUrl}
|
||||
isGif={isGif}
|
||||
posterData={bestImageData}
|
||||
@ -257,6 +179,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
noPlay={!isActive}
|
||||
onClose={onClose}
|
||||
isMuted={isMuted}
|
||||
isProtected={isProtected}
|
||||
volume={volume}
|
||||
playbackRate={playbackRate}
|
||||
/>
|
||||
@ -265,6 +188,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
<MediaViewerFooter
|
||||
text={textParts}
|
||||
onClick={onFooterClick}
|
||||
isProtected={isProtected}
|
||||
isHidden={isFooterHidden}
|
||||
isForVideo={isVideo && !isGif}
|
||||
/>
|
||||
@ -278,9 +202,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const {
|
||||
chatId,
|
||||
threadId,
|
||||
messageId,
|
||||
mediaId,
|
||||
avatarOwnerId,
|
||||
profilePhotoIndex,
|
||||
origin,
|
||||
} = ownProps;
|
||||
|
||||
@ -291,18 +214,18 @@ export default memo(withGlobal<OwnProps>(
|
||||
} = global.mediaViewer;
|
||||
|
||||
if (origin === MediaViewerOrigin.SearchResult) {
|
||||
if (!(chatId && messageId)) {
|
||||
if (!(chatId && mediaId)) {
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
const message = selectChatMessage(global, chatId, mediaId);
|
||||
if (!message) {
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
return {
|
||||
chatId,
|
||||
messageId,
|
||||
mediaId,
|
||||
senderId: message.senderId,
|
||||
origin,
|
||||
message,
|
||||
@ -317,10 +240,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
const sender = selectUser(global, avatarOwnerId) || selectChat(global, avatarOwnerId);
|
||||
|
||||
return {
|
||||
messageId: -1,
|
||||
mediaId,
|
||||
senderId: avatarOwnerId,
|
||||
avatarOwner: sender,
|
||||
profilePhotoIndex: profilePhotoIndex || 0,
|
||||
origin,
|
||||
volume,
|
||||
isMuted,
|
||||
@ -328,15 +250,15 @@ export default memo(withGlobal<OwnProps>(
|
||||
};
|
||||
}
|
||||
|
||||
if (!(chatId && threadId && messageId)) {
|
||||
if (!(chatId && threadId && mediaId)) {
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
let message: ApiMessage | undefined;
|
||||
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
|
||||
message = selectScheduledMessage(global, chatId, messageId);
|
||||
message = selectScheduledMessage(global, chatId, mediaId);
|
||||
} else {
|
||||
message = selectChatMessage(global, chatId, messageId);
|
||||
message = selectChatMessage(global, chatId, mediaId);
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
@ -346,7 +268,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
return {
|
||||
chatId,
|
||||
threadId,
|
||||
messageId,
|
||||
mediaId,
|
||||
senderId: message.senderId,
|
||||
origin,
|
||||
message,
|
||||
@ -358,15 +280,19 @@ export default memo(withGlobal<OwnProps>(
|
||||
},
|
||||
)(MediaViewerContent));
|
||||
|
||||
function renderPhoto(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean) {
|
||||
function renderPhoto(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean, isProtected?: boolean) {
|
||||
return blobUrl
|
||||
? (
|
||||
<img
|
||||
src={blobUrl}
|
||||
alt=""
|
||||
style={imageSize ? `width: ${imageSize.width}px` : ''}
|
||||
draggable={Boolean(canDrag)}
|
||||
/>
|
||||
<div style="position: relative;">
|
||||
{isProtected && <div onContextMenu={stopEvent} className="protector" />}
|
||||
<img
|
||||
src={blobUrl}
|
||||
alt=""
|
||||
className={buildClassName(isProtected && 'is-protected')}
|
||||
style={imageSize ? `width: ${imageSize.width}px` : ''}
|
||||
draggable={Boolean(canDrag)}
|
||||
/>
|
||||
</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 videoStyle = `background-image: url(${blobUrl})`;
|
||||
return blobUrl
|
||||
@ -386,12 +312,14 @@ function renderVideoPreview(blobUrl?: string, imageSize?: ApiDimensions, canDrag
|
||||
<div
|
||||
className="VideoPlayer"
|
||||
>
|
||||
{isProtected && <div onContextMenu={stopEvent} className="protector" />}
|
||||
<div
|
||||
style={wrapperStyle}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video
|
||||
style={videoStyle}
|
||||
className={buildClassName(isProtected && 'is-protected')}
|
||||
draggable={Boolean(canDrag)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -17,10 +17,11 @@ type OwnProps = {
|
||||
onClick: () => void;
|
||||
isHidden?: boolean;
|
||||
isForVideo: boolean;
|
||||
isProtected?: boolean;
|
||||
};
|
||||
|
||||
const MediaViewerFooter: FC<OwnProps> = ({
|
||||
text = '', isHidden, isForVideo, onClick,
|
||||
text = '', isHidden, isForVideo, onClick, isProtected,
|
||||
}) => {
|
||||
const [isMultiline, setIsMultiline] = useState(false);
|
||||
useEffect(() => {
|
||||
@ -54,6 +55,7 @@ const MediaViewerFooter: FC<OwnProps> = ({
|
||||
'MediaViewerFooter',
|
||||
isForVideo && 'is-for-video',
|
||||
isHidden && 'is-hidden',
|
||||
isProtected && 'is-protected',
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -27,18 +27,16 @@ import './MediaViewerSlides.scss';
|
||||
const { easeOutCubic, easeOutQuart } = timingFunctions;
|
||||
|
||||
type OwnProps = {
|
||||
messageId?: number;
|
||||
getMessageId: (fromId?: number, direction?: number) => number | undefined;
|
||||
mediaId?: number;
|
||||
getMediaId: (fromId?: number, direction?: number) => number | undefined;
|
||||
isVideo?: boolean;
|
||||
isGif?: boolean;
|
||||
isPhoto?: boolean;
|
||||
isOpen?: boolean;
|
||||
selectMessage: (id?: number) => void;
|
||||
selectMedia: (id?: number) => void;
|
||||
chatId?: string;
|
||||
threadId?: number;
|
||||
isActive?: boolean;
|
||||
avatarOwnerId?: string;
|
||||
profilePhotoIndex?: number;
|
||||
origin?: MediaViewerOrigin;
|
||||
animationLevel: 0 | 1 | 2;
|
||||
onClose: () => void;
|
||||
@ -74,14 +72,13 @@ enum SwipeDirection {
|
||||
}
|
||||
|
||||
const MediaViewerSlides: FC<OwnProps> = ({
|
||||
messageId,
|
||||
getMessageId,
|
||||
selectMessage,
|
||||
mediaId,
|
||||
getMediaId,
|
||||
selectMedia,
|
||||
isVideo,
|
||||
isGif,
|
||||
isPhoto,
|
||||
isOpen,
|
||||
isActive,
|
||||
hasFooter,
|
||||
zoomLevelChange,
|
||||
animationLevel,
|
||||
@ -96,7 +93,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
const swipeDirectionRef = useRef<SwipeDirection | undefined>(undefined);
|
||||
const isActiveRef = useRef(true);
|
||||
const isReleasedRef = useRef(false);
|
||||
const [activeMessageId, setActiveMessageId] = useState<number | undefined>(messageId);
|
||||
const [activeMediaId, setActiveMediaId] = useState<number | undefined>(mediaId);
|
||||
const prevZoomLevelChange = usePrevious(zoomLevelChange);
|
||||
const hasZoomChanged = prevZoomLevelChange !== undefined && prevZoomLevelChange !== zoomLevelChange;
|
||||
const forceUpdate = useForceUpdate();
|
||||
@ -112,7 +109,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
forceUpdate();
|
||||
}, [forceUpdate]);
|
||||
|
||||
const selectMessageDebounced = useDebouncedCallback(selectMessage, [], DEBOUNCE_MESSAGE, true);
|
||||
const selectMediaDebounced = useDebouncedCallback(selectMedia, [], DEBOUNCE_MESSAGE, true);
|
||||
const clearSwipeDirectionDebounced = useDebouncedCallback(() => {
|
||||
swipeDirectionRef.current = undefined;
|
||||
}, [], DEBOUNCE_SWIPE, true);
|
||||
@ -135,7 +132,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
useTimeout(() => setIsFooterHidden(false), ANIMATION_DURATION - 150);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !activeMessageId) {
|
||||
if (!containerRef.current || activeMediaId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
let lastTransform = lastTransformRef.current;
|
||||
@ -159,13 +156,13 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}, 500, false, true);
|
||||
|
||||
const changeSlide = (direction: number) => {
|
||||
const mId = getMessageId(activeMessageId, direction);
|
||||
if (mId) {
|
||||
const mId = getMediaId(activeMediaId, direction);
|
||||
if (mId !== undefined) {
|
||||
const offset = (windowWidth + SLIDES_GAP) * direction;
|
||||
transformRef.current.x += offset;
|
||||
isActiveRef.current = false;
|
||||
setActiveMessageId(mId);
|
||||
selectMessageDebounced(mId);
|
||||
setActiveMediaId(mId);
|
||||
selectMediaDebounced(mId);
|
||||
setIsActiveDebounced(true);
|
||||
lastTransform = { x: 0, y: 0, scale: 1 };
|
||||
if (animationLevel === 0) {
|
||||
@ -347,19 +344,19 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}
|
||||
// Get horizontal swipe direction
|
||||
const direction = x < 0 ? 1 : -1;
|
||||
const mId = getMessageId(activeMessageId, x < 0 ? 1 : -1);
|
||||
const mId = getMediaId(activeMediaId, x < 0 ? 1 : -1);
|
||||
// Get the direction of the last pan gesture.
|
||||
// Could be different from the total horizontal swipe direction
|
||||
// if user starts a swipe in one direction and then changes the direction
|
||||
// we need to cancel slide transition
|
||||
const dirX = panDelta.x < 0 ? -1 : 1;
|
||||
if (mId && absX >= SWIPE_X_THRESHOLD && direction === dirX) {
|
||||
if (mId !== undefined && absX >= SWIPE_X_THRESHOLD && direction === dirX) {
|
||||
const offset = (windowWidth + SLIDES_GAP) * direction;
|
||||
// If image is shifted by more than SWIPE_X_THRESHOLD,
|
||||
// We shift everything by one screen width and then set new active message id
|
||||
transformRef.current.x += offset;
|
||||
setActiveMessageId(mId);
|
||||
selectMessageDebounced(mId);
|
||||
setActiveMediaId(mId);
|
||||
selectMediaDebounced(mId);
|
||||
}
|
||||
// Then we always return to the original position
|
||||
cancelAnimation = animateNumber({
|
||||
@ -606,13 +603,13 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}, [
|
||||
onClose,
|
||||
setTransform,
|
||||
getMessageId,
|
||||
activeMessageId,
|
||||
getMediaId,
|
||||
activeMediaId,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
clickXThreshold,
|
||||
shouldCloseOnVideo,
|
||||
selectMessageDebounced,
|
||||
selectMediaDebounced,
|
||||
setIsActiveDebounced,
|
||||
clearSwipeDirectionDebounced,
|
||||
animationLevel,
|
||||
@ -623,12 +620,13 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
if (!containerRef.current || !hasZoomChanged) return;
|
||||
const { scale } = transformRef.current;
|
||||
const dir = zoomLevelChange > 0 ? -1 : +1;
|
||||
const minZoom = MIN_ZOOM * 0.5;
|
||||
const minZoom = MIN_ZOOM * 0.6;
|
||||
const maxZoom = MAX_ZOOM * 3;
|
||||
const steps = 100;
|
||||
let steps = 100;
|
||||
let prevValue = 0;
|
||||
if (scale <= minZoom && dir > 0) return;
|
||||
if (scale >= maxZoom && dir < 0) return;
|
||||
if (scale === 1 && dir > 0) steps = 20;
|
||||
if (cancelZoomAnimation) cancelZoomAnimation();
|
||||
cancelZoomAnimation = animateNumber({
|
||||
from: dir,
|
||||
@ -649,61 +647,61 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
});
|
||||
}, [zoomLevelChange, hasZoomChanged]);
|
||||
|
||||
if (!activeMessageId) return undefined;
|
||||
if (activeMediaId === undefined) return undefined;
|
||||
|
||||
const nextMessageId = getMessageId(activeMessageId, 1);
|
||||
const previousMessageId = getMessageId(activeMessageId, -1);
|
||||
const nextMediaId = getMediaId(activeMediaId, 1);
|
||||
const prevMediaId = getMediaId(activeMediaId, -1);
|
||||
const hasPrev = prevMediaId !== undefined;
|
||||
const hasNext = nextMediaId !== undefined;
|
||||
const offsetX = transformRef.current.x;
|
||||
const offsetY = transformRef.current.y;
|
||||
const { scale } = transformRef.current;
|
||||
|
||||
return (
|
||||
<div className="MediaViewerSlides" ref={containerRef}>
|
||||
{previousMessageId && scale === 1 && !isResizing && (
|
||||
{hasPrev && scale === 1 && !isResizing && (
|
||||
<div className="MediaViewerSlide" style={getAnimationStyle(-windowWidth + offsetX - SLIDES_GAP)}>
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
animationLevel={animationLevel}
|
||||
isFooterHidden={isFooterHidden}
|
||||
messageId={previousMessageId}
|
||||
mediaId={prevMediaId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeMessageId && (
|
||||
<div
|
||||
className={buildClassName(
|
||||
'MediaViewerSlide',
|
||||
isActive && 'MediaViewerSlide--active',
|
||||
isMouseDown && scale > 1 && 'MediaViewerSlide--moving',
|
||||
)}
|
||||
onClick={handleToggleFooterVisibility}
|
||||
ref={activeSlideRef}
|
||||
style={getAnimationStyle(offsetX, offsetY, scale)}
|
||||
>
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
messageId={activeMessageId}
|
||||
animationLevel={animationLevel}
|
||||
isActive={isActive && isActiveRef.current}
|
||||
setIsFooterHidden={setIsFooterHidden}
|
||||
isFooterHidden={isFooterHidden || scale !== 1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{nextMessageId && scale === 1 && !isResizing && (
|
||||
<div
|
||||
className={buildClassName(
|
||||
'MediaViewerSlide',
|
||||
'MediaViewerSlide--active',
|
||||
isMouseDown && scale > 1 && 'MediaViewerSlide--moving',
|
||||
)}
|
||||
onClick={handleToggleFooterVisibility}
|
||||
ref={activeSlideRef}
|
||||
style={getAnimationStyle(offsetX, offsetY, scale)}
|
||||
>
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
mediaId={activeMediaId}
|
||||
animationLevel={animationLevel}
|
||||
isActive={isActiveRef.current}
|
||||
setIsFooterHidden={setIsFooterHidden}
|
||||
isFooterHidden={isFooterHidden || scale !== 1}
|
||||
/>
|
||||
</div>
|
||||
{hasNext && scale === 1 && !isResizing && (
|
||||
<div className="MediaViewerSlide" style={getAnimationStyle(windowWidth + offsetX + SLIDES_GAP)}>
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
animationLevel={animationLevel}
|
||||
isFooterHidden={isFooterHidden}
|
||||
messageId={nextMessageId}
|
||||
mediaId={nextMediaId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{previousMessageId && scale === 1 && !IS_TOUCH_ENV && (
|
||||
{hasPrev && scale === 1 && !IS_TOUCH_ENV && (
|
||||
<button
|
||||
type="button"
|
||||
className={`navigation prev ${isVideo && !isGif && 'inline'}`}
|
||||
@ -711,7 +709,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
/>
|
||||
)}
|
||||
{nextMessageId && scale === 1 && !IS_TOUCH_ENV && (
|
||||
{hasNext && scale === 1 && !IS_TOUCH_ENV && (
|
||||
<button
|
||||
type="button"
|
||||
className={`navigation next ${isVideo && !isGif && 'inline'}`}
|
||||
|
||||
@ -64,6 +64,7 @@
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
background-color: rgba(0, 0, 0, 0.5) !important;
|
||||
z-index: 3;
|
||||
body:not(.animation-level-0) & {
|
||||
transition: opacity 0.3s ease !important;
|
||||
}
|
||||
|
||||
@ -12,14 +12,14 @@ import useShowTransition from '../../hooks/useShowTransition';
|
||||
import useVideoCleanup from '../../hooks/useVideoCleanup';
|
||||
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
|
||||
import safePlay from '../../util/safePlay';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import ProgressSpinner from '../ui/ProgressSpinner';
|
||||
import VideoPlayerControls from './VideoPlayerControls';
|
||||
|
||||
import './VideoPlayer.scss';
|
||||
|
||||
import VideoPlayerControls from './VideoPlayerControls';
|
||||
|
||||
type OwnProps = {
|
||||
url?: string;
|
||||
isGif?: boolean;
|
||||
@ -33,6 +33,7 @@ type OwnProps = {
|
||||
volume: number;
|
||||
isMuted: boolean;
|
||||
playbackRate: number;
|
||||
isProtected?: boolean;
|
||||
toggleControls: (isVisible: boolean) => void;
|
||||
onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
};
|
||||
@ -54,6 +55,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
onClose,
|
||||
toggleControls,
|
||||
areControlsVisible,
|
||||
isProtected,
|
||||
}) => {
|
||||
const {
|
||||
setMediaViewerVolume,
|
||||
@ -181,9 +183,18 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
style={wrapperStyle}
|
||||
>
|
||||
{/* 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
|
||||
ref={videoRef}
|
||||
autoPlay={IS_TOUCH_ENV}
|
||||
controlsList={isProtected ? 'nodownload' : undefined}
|
||||
playsInline
|
||||
loop={isGif}
|
||||
// This is to force auto playing on mobiles
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
font-size: 0.875rem;
|
||||
background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%);
|
||||
transition: opacity 0.3s;
|
||||
z-index: var(--z-video-player-controls);
|
||||
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
@ -20,7 +22,6 @@
|
||||
position: fixed;
|
||||
padding: 2.25rem 0.5rem 0.75rem;
|
||||
background: none;
|
||||
z-index: var(--z-media-viewer);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@ -147,8 +148,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.playback-rate-menu .bubble {
|
||||
min-width: 4rem;
|
||||
margin-right: 4rem;
|
||||
.playback-rate-menu {
|
||||
.bubble {
|
||||
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>
|
||||
<Menu
|
||||
isOpen={isPlaybackMenuOpen}
|
||||
className="playback-rate-menu"
|
||||
className={buildClassName('playback-rate-menu', !isFullscreenSupported && 'no-fullscreen')}
|
||||
positionX="right"
|
||||
positionY="bottom"
|
||||
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 => {
|
||||
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]);
|
||||
|
||||
@ -87,7 +90,7 @@ export default function useInnerHandlers(
|
||||
openMediaViewer({
|
||||
chatId,
|
||||
threadId,
|
||||
messageId: albumMessageId,
|
||||
mediaId: albumMessageId,
|
||||
origin: isScheduled ? MediaViewerOrigin.ScheduledAlbum : MediaViewerOrigin.Album,
|
||||
});
|
||||
}, [chatId, threadId, openMediaViewer, isScheduled]);
|
||||
|
||||
@ -225,11 +225,11 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [loadProfilePhotos, profileId, lastSyncTime]);
|
||||
|
||||
const handleSelectMedia = useCallback((messageId: number) => {
|
||||
const handleSelectMedia = useCallback((mediaId: number) => {
|
||||
openMediaViewer({
|
||||
chatId: profileId,
|
||||
threadId: MAIN_THREAD_ID,
|
||||
messageId,
|
||||
mediaId,
|
||||
origin: MediaViewerOrigin.SharedMedia,
|
||||
});
|
||||
}, [profileId, openMediaViewer]);
|
||||
|
||||
@ -2,7 +2,7 @@ import { addActionHandler } from '../../index';
|
||||
|
||||
addActionHandler('openMediaViewer', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin, volume, playbackRate, isMuted,
|
||||
chatId, threadId, mediaId, avatarOwnerId, profilePhotoIndex, origin, volume, playbackRate, isMuted,
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
@ -11,7 +11,7 @@ addActionHandler('openMediaViewer', (global, actions, payload) => {
|
||||
...global.mediaViewer,
|
||||
chatId,
|
||||
threadId,
|
||||
messageId,
|
||||
mediaId,
|
||||
avatarOwnerId,
|
||||
profilePhotoIndex,
|
||||
origin,
|
||||
|
||||
@ -10,7 +10,7 @@ import { selectCurrentManagement } from './management';
|
||||
|
||||
export function selectIsMediaViewerOpen(global: GlobalState) {
|
||||
const { mediaViewer } = global;
|
||||
return Boolean(mediaViewer.messageId || mediaViewer.avatarOwnerId);
|
||||
return Boolean(mediaViewer.mediaId || mediaViewer.avatarOwnerId);
|
||||
}
|
||||
|
||||
export function selectRightColumnContentKey(global: GlobalState) {
|
||||
|
||||
@ -399,7 +399,7 @@ export type GlobalState = {
|
||||
mediaViewer: {
|
||||
chatId?: string;
|
||||
threadId?: number;
|
||||
messageId?: number;
|
||||
mediaId?: number;
|
||||
avatarOwnerId?: string;
|
||||
profilePhotoIndex?: number;
|
||||
origin?: MediaViewerOrigin;
|
||||
@ -750,7 +750,7 @@ export interface ActionPayloads {
|
||||
openMediaViewer: {
|
||||
chatId?: string;
|
||||
threadId?: number;
|
||||
messageId?: number;
|
||||
mediaId?: number;
|
||||
avatarOwnerId?: string;
|
||||
profilePhotoIndex?: number;
|
||||
origin?: MediaViewerOrigin;
|
||||
|
||||
@ -211,6 +211,7 @@ $color-message-reaction-own-hover: #b5e0a4;
|
||||
--z-header-menu-backdrop: 980;
|
||||
--z-modal: 1510;
|
||||
--z-media-viewer: 1500;
|
||||
--z-video-player-controls: 3;
|
||||
--z-drop-area: 55;
|
||||
--z-animation-fade: 50;
|
||||
--z-menu-bubble: 21;
|
||||
|
||||
@ -265,6 +265,8 @@ div[role="button"] {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.for-ios-autocapitalization-fix {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user