diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index f655d447f..542020e44 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -395,11 +395,38 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u } const { - id, size, mimeType, date, thumbs, + id, size, mimeType, date, thumbs, attributes, } = document; const thumbnail = thumbs && buildApiThumbnailFromStripped(thumbs); + let mediaType: ApiDocument['mediaType'] | undefined; + let mediaSize: ApiDocument['mediaSize'] | undefined; + const photoSize = thumbs && thumbs.find((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize); + if (photoSize) { + mediaSize = { + width: photoSize.w, + height: photoSize.h, + }; + } + + if (mimeType.startsWith('image/')) { + mediaType = 'photo'; + + const imageAttribute = attributes + .find((a: any): a is GramJs.DocumentAttributeImageSize => a instanceof GramJs.DocumentAttributeImageSize); + + if (imageAttribute) { + const { w: width, h: height } = imageAttribute; + mediaSize = { + width, + height, + }; + } + } else if (mimeType.startsWith('video/')) { + mediaType = 'video'; + } + return { id: String(id), size, @@ -407,6 +434,8 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u timestamp: date, fileName: getFilenameFromDocument(document), thumbnail, + mediaType, + mediaSize, }; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 7ad70fb74..15e527f70 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -1,13 +1,14 @@ -export interface ApiPhotoSize { - type: 's' | 'm' | 'x' | 'y' | 'z'; +export interface ApiDimensions { width: number; height: number; } -export interface ApiThumbnail { +export interface ApiPhotoSize extends ApiDimensions { + type: 's' | 'm' | 'x' | 'y' | 'z'; +} + +export interface ApiThumbnail extends ApiDimensions { dataUri: string; - height: number; - width: number; } export interface ApiPhoto { @@ -81,6 +82,8 @@ export interface ApiDocument { mimeType: string; thumbnail?: ApiThumbnail; previewBlobUrl?: string; + mediaType?: 'photo' | 'video'; + mediaSize?: ApiDimensions; } export interface ApiContact { diff --git a/src/components/common/Document.tsx b/src/components/common/Document.tsx index 5f72f83ec..3c5b5a1b3 100644 --- a/src/components/common/Document.tsx +++ b/src/components/common/Document.tsx @@ -5,7 +5,12 @@ import React, { import { ApiMessage } from '../../api/types'; import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo'; -import { getMediaTransferState, getMessageMediaHash, getMessageMediaThumbDataUri } from '../../modules/helpers'; +import { + getMediaTransferState, + getMessageMediaHash, + getMessageMediaThumbDataUri, + isMessageDocumentVideo, +} from '../../modules/helpers'; import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver'; import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgress'; import useMedia from '../../hooks/useMedia'; @@ -25,6 +30,7 @@ type OwnProps = { className?: string; sender?: string; onCancelUpload?: () => void; + onMediaClick?: () => void; onDateClick?: (messageId: number, chatId: number) => void; }; @@ -40,6 +46,7 @@ const Document: FC = ({ isSelected, isSelectable, onCancelUpload, + onMediaClick, onDateClick, }) => { // eslint-disable-next-line no-null/no-null @@ -48,6 +55,7 @@ const Document: FC = ({ const document = message.content.document!; const extension = getDocumentExtension(document) || ''; const { fileName, size, timestamp } = document; + const withMediaViewer = onMediaClick && Boolean(document.mediaType); const isIntersecting = useIsIntersecting(ref, observeIntersection); @@ -65,14 +73,16 @@ const Document: FC = ({ const previewData = useMedia(getMessageMediaHash(message, 'pictogram'), !isIntersecting); const handleClick = useCallback(() => { - if (isUploading) { + if (withMediaViewer) { + onMediaClick!(); + } else if (isUploading) { if (onCancelUpload) { onCancelUpload(); } } else { setIsDownloadAllowed((isAllowed) => !isAllowed); } - }, [isUploading, onCancelUpload]); + }, [withMediaViewer, isUploading, onCancelUpload, onMediaClick]); const handleDateClick = useCallback(() => { onDateClick!(message.id, message.chatId); @@ -102,6 +112,7 @@ const Document: FC = ({ sender={sender} isSelectable={isSelectable} isSelected={isSelected} + actionIcon={withMediaViewer ? (isMessageDocumentVideo(message) ? 'icon-play' : 'icon-eye') : 'icon-download'} onClick={handleClick} onDateClick={onDateClick ? handleDateClick : undefined} /> diff --git a/src/components/common/File.scss b/src/components/common/File.scss index dad721a96..123fcb284 100644 --- a/src/components/common/File.scss +++ b/src/components/common/File.scss @@ -70,7 +70,7 @@ background: transparent; overflow: hidden; - & + .icon-download, + & + .action-icon, & + .file-progress { background: rgba(black, 0.5); border-radius: var(--border-radius-messages-small); @@ -81,7 +81,7 @@ } } - .icon-download { + .action-icon { color: #fff; font-size: 1.5rem; position: absolute; @@ -104,7 +104,7 @@ border-width: 0; } - .icon-download { + .action-icon { opacity: 1; &.hidden { @@ -164,7 +164,7 @@ --background-color: var(--color-background); --border-radius-messages-small: .3125rem; - .icon-download, + .action-icon, .file-progress, .file-icon, .file-preview { @@ -211,7 +211,7 @@ } } - .icon-download { + .action-icon { left: auto; right: 0; } diff --git a/src/components/common/File.tsx b/src/components/common/File.tsx index 7979f5174..829b85ce1 100644 --- a/src/components/common/File.tsx +++ b/src/components/common/File.tsx @@ -31,6 +31,7 @@ type OwnProps = { isSelectable?: boolean; isSelected?: boolean; transferProgress?: number; + actionIcon?: string; onClick?: () => void; onDateClick?: (e: React.MouseEvent) => void; }; @@ -51,6 +52,7 @@ const File: FC = ({ isSelectable, isSelected, transferProgress, + actionIcon, onClick, onDateClick, }) => { @@ -126,7 +128,15 @@ const File: FC = ({ /> )} - {onClick && } + {onClick && ( + + )}
{renderText(name)}
diff --git a/src/components/common/helpers/mediaDimensions.ts b/src/components/common/helpers/mediaDimensions.ts index 7877dc6da..c721a7928 100644 --- a/src/components/common/helpers/mediaDimensions.ts +++ b/src/components/common/helpers/mediaDimensions.ts @@ -1,16 +1,19 @@ -import { ApiPhoto, ApiVideo, ApiSticker } from '../../../api/types'; -import { getPhotoInlineDimensions, getVideoDimensions, IDimensions } from '../../../modules/helpers'; -import windowSize from '../../../util/windowSize'; -import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; +import { + ApiPhoto, ApiVideo, ApiSticker, ApiDimensions, +} from '../../../api/types'; + import { STICKER_SIZE_INLINE_DESKTOP_FACTOR, STICKER_SIZE_INLINE_MOBILE_FACTOR } from '../../../config'; +import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; +import windowSize from '../../../util/windowSize'; +import { getPhotoInlineDimensions, getVideoDimensions } from '../../../modules/helpers'; export const MEDIA_VIEWER_MEDIA_QUERY = '(max-height: 640px)'; export const REM = parseInt(getComputedStyle(document.documentElement).fontSize, 10); export const ROUND_VIDEO_DIMENSIONS = 200; export const AVATAR_FULL_DIMENSIONS = { width: 640, height: 640 }; - -const DEFAULT_MEDIA_DIMENSIONS: IDimensions = { width: 100, height: 100 }; export const LIKE_STICKER_ID = '1258816259753933'; + +const DEFAULT_MEDIA_DIMENSIONS: ApiDimensions = { width: 100, height: 100 }; const MOBILE_SCREEN_NO_AVATARS_MESSAGE_EXTRA_WIDTH_REM = 4.5; const MOBILE_SCREEN_MESSAGE_EXTRA_WIDTH_REM = 7; const MESSAGE_MAX_WIDTH_REM = 29; @@ -92,7 +95,7 @@ function calculateDimensionsForMessageMedia({ isWebPagePhoto?: boolean; isGif?: boolean; noAvatars?: boolean; -}): IDimensions { +}): ApiDimensions { const aspectRatio = height / width; const availableWidth = getAvailableWidth(fromOwnMessage, isForwarded, isWebPagePhoto, noAvatars); const availableHeight = getAvailableHeight(isGif, aspectRatio); @@ -100,7 +103,7 @@ function calculateDimensionsForMessageMedia({ return calculateDimensions(availableWidth, availableHeight, width, height); } -export function getMediaViewerAvailableDimensions(withFooter: boolean, isVideo: boolean): IDimensions { +export function getMediaViewerAvailableDimensions(withFooter: boolean, isVideo: boolean): ApiDimensions { const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY); const { width: windowWidth, height: windowHeight } = windowSize.get(); let occupiedHeightRem = isVideo && mql.matches ? 10 : 8.25; @@ -151,14 +154,14 @@ export function calculateVideoDimensions( }); } -export function getPictogramDimensions(): IDimensions { +export function getPictogramDimensions(): ApiDimensions { return { width: 2 * REM, height: 2 * REM, }; } -export function getDocumentThumbnailDimensions(smaller?: boolean): IDimensions { +export function getDocumentThumbnailDimensions(smaller?: boolean): ApiDimensions { if (smaller) { return { width: 3 * REM, @@ -172,7 +175,7 @@ export function getDocumentThumbnailDimensions(smaller?: boolean): IDimensions { }; } -export function getStickerDimensions(sticker: ApiSticker): IDimensions { +export function getStickerDimensions(sticker: ApiSticker): ApiDimensions { const { width } = sticker; let { height } = sticker; @@ -203,8 +206,8 @@ export function getStickerDimensions(sticker: ApiSticker): IDimensions { } export function calculateMediaViewerDimensions( - { width, height }: IDimensions, withFooter: boolean, isVideo: boolean = false, -): IDimensions { + { width, height }: ApiDimensions, withFooter: boolean, isVideo: boolean = false, +): ApiDimensions { const { width: availableWidth, height: availableHeight } = getMediaViewerAvailableDimensions(withFooter, isVideo); return calculateDimensions(availableWidth, availableHeight, width, height); @@ -215,7 +218,7 @@ export function calculateDimensions( availableHeight: number, mediaWidth: number, mediaHeight: number, -): IDimensions { +): ApiDimensions { const aspectRatio = mediaHeight / mediaWidth; const calculatedWidth = Math.min(mediaWidth, availableWidth); const calculatedHeight = Math.round(calculatedWidth * aspectRatio); diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index ca9f8c4ad..34669fd4f 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -5,7 +5,7 @@ import { withGlobal } from '../../lib/teact/teactn'; import { GlobalActions } from '../../global/types'; import { - ApiChat, ApiMediaFormat, ApiMessage, ApiUser, + ApiChat, ApiMediaFormat, ApiMessage, ApiUser, ApiDimensions, } from '../../api/types'; import { MediaViewerOrigin } from '../../types'; @@ -30,17 +30,20 @@ import { import { getChatAvatarHash, getChatMediaMessageIds, - getMessageMediaFilename, + getMessageFileName, getMessageMediaFormat, getMessageMediaHash, getMessageMediaThumbDataUri, getMessagePhoto, getMessageVideo, + getMessageDocument, + isMessageDocumentPhoto, + isMessageDocumentVideo, getMessageWebPagePhoto, getMessageWebPageVideo, getPhotoFullDimensions, - getVideoDimensions, - IDimensions, + getVideoDimensions, getMessageFileSize, + } from '../../modules/helpers'; import { pick } from '../../util/iteratees'; import { captureEvents, SwipeDirection } from '../../util/captureEvents'; @@ -104,56 +107,62 @@ const MediaViewer: FC = ({ focusMessage, animationLevel, }) => { - // eslint-disable-next-line no-null/no-null - const animationKey = useRef(null); const isOpen = Boolean(avatarOwner || messageId); - const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined; - const webPageVideo = message ? getMessageWebPageVideo(message) : undefined; - const photo = message ? getMessagePhoto(message) : undefined; - const video = message ? getMessageVideo(message) : undefined; - const isWebPagePhoto = Boolean(webPagePhoto); - const isWebPageVideo = Boolean(webPageVideo); - const messageVideo = video || webPageVideo; - const isVideo = Boolean(messageVideo); - const isPhoto = Boolean(!isVideo && (photo || webPagePhoto)); - const isGif = (messageVideo) ? messageVideo.isGif : undefined; + const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia; const isFromSearch = origin === MediaViewerOrigin.SearchResult; - const slideAnimation = animationLevel >= 1 ? 'mv-slide' : 'none'; - const headerAnimation = animationLevel === 2 ? 'slide-fade' : 'none'; - const isGhostAnimation = animationLevel === 2; - const fileName = avatarOwner - ? `avatar${avatarOwner.id}-${profilePhotoIndex}.jpg` - : message && getMessageMediaFilename(message); - const prevSenderId = usePrevious(senderId); - const [canPanZoomWrap, setCanPanZoomWrap] = useState(false); - const [isZoomed, setIsZoomed] = useState(false); - const [zoomLevel, setZoomLevel] = useState(1); - const [panDelta, setPanDelta] = useState({ x: 0, y: 0 }); - const [isFooterHidden, setIsFooterHidden] = useState(false); + /* 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 isAvatar = Boolean(avatarOwner); + + /* Navigation */ + const isSingleSlide = Boolean(webPagePhoto || webPageVideo); const messageIds = useMemo(() => { - return (isWebPagePhoto || isWebPageVideo) && messageId + return isSingleSlide && messageId ? [messageId] : getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia); - }, [isWebPagePhoto, isWebPageVideo, messageId, chatMessages, collectionIds, isFromSharedMedia]); + }, [isSingleSlide, messageId, chatMessages, collectionIds, isFromSharedMedia]); const selectedMediaMessageIndex = messageId ? messageIds.indexOf(messageId) : -1; const isFirst = selectedMediaMessageIndex === 0 || selectedMediaMessageIndex === -1; const isLast = selectedMediaMessageIndex === messageIds.length - 1 || selectedMediaMessageIndex === -1; + + /* Animation */ + const animationKey = useRef(); + const prevSenderId = usePrevious(senderId); if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) { animationKey.current = selectedMediaMessageIndex; } + const slideAnimation = animationLevel >= 1 ? 'mv-slide' : 'none'; + const headerAnimation = animationLevel === 2 ? 'slide-fade' : 'none'; + const isGhostAnimation = animationLevel === 2; - function getMediaHash(full?: boolean) { - if (avatarOwner && profilePhotoIndex !== undefined) { - const { photos } = avatarOwner; + /* Controls */ + const [isFooterHidden, setIsFooterHidden] = useState(false); + const [canPanZoomWrap, setCanPanZoomWrap] = useState(false); + const [isZoomed, setIsZoomed] = useState(false); + const [zoomLevel, setZoomLevel] = useState(1); + const [panDelta, setPanDelta] = useState({ x: 0, y: 0 }); + + /* Media data */ + function getMediaHash(isFull?: boolean) { + if (isAvatar && profilePhotoIndex !== undefined) { + const { photos } = avatarOwner!; return photos && photos[profilePhotoIndex] ? `photo${photos[profilePhotoIndex].id}?size=c` - : getChatAvatarHash(avatarOwner, full ? 'big' : 'normal'); + : getChatAvatarHash(avatarOwner!, isFull ? 'big' : 'normal'); } - return message && getMessageMediaHash(message, full ? 'viewerFull' : 'viewerPreview'); + return message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview'); } const blobUrlPictogram = useMedia( @@ -167,7 +176,7 @@ const MediaViewer: FC = ({ const blobUrlPreview = useMedia( previewMediaHash, undefined, - avatarOwner && previewMediaHash && previewMediaHash.startsWith('profilePhoto') + isAvatar && previewMediaHash && previewMediaHash.startsWith('profilePhoto') ? ApiMediaFormat.DataUri : ApiMediaFormat.BlobUrl, undefined, @@ -188,12 +197,25 @@ const MediaViewer: FC = ({ bestImageData = thumbDataUri; } - const photoDimensions = isPhoto ? getPhotoFullDimensions(( - isWebPagePhoto ? webPagePhoto : photo - )!) : undefined; - const videoDimensions = isVideo ? getVideoDimensions(( - isWebPageVideo ? webPageVideo : video - )!) : undefined; + const videoSize = message ? getMessageFileSize(message) : undefined; + const fileName = message + ? getMessageFileName(message) + : isAvatar + ? `avatar${avatarOwner!.id}-${profilePhotoIndex}.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 = AVATAR_FULL_DIMENSIONS; + } useEffect(() => { if (!IS_SINGLE_COLUMN_LAYOUT) { @@ -230,7 +252,7 @@ const MediaViewer: FC = ({ dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY); const textParts = message ? renderMessageText(message) : undefined; const hasFooter = Boolean(textParts); - animateOpening(hasFooter, origin!, bestImageData!, message); + animateOpening(hasFooter, origin!, bestImageData!, dimensions, isVideo, message); } if (isGhostAnimation && !isOpen && (prevMessage || prevAvatarOwner)) { @@ -238,8 +260,8 @@ const MediaViewer: FC = ({ animateClosing(prevOrigin!, prevBestImageData!, prevMessage || undefined); } }, [ - isGhostAnimation, isOpen, origin, prevOrigin, - message, prevMessage, prevAvatarOwner, bestImageData, prevBestImageData, + isGhostAnimation, isOpen, origin, prevOrigin, message, prevMessage, prevAvatarOwner, + bestImageData, prevBestImageData, dimensions, isVideo, ]); useEffect(() => { @@ -409,7 +431,7 @@ const MediaViewer: FC = ({ const lang = useLang(); function renderSlide(isActive: boolean) { - if (avatarOwner) { + if (isAvatar) { return (
{renderPhoto( @@ -431,7 +453,7 @@ const MediaViewer: FC = ({ > {isPhoto && renderPhoto( localBlobUrl || fullMediaData || blobUrlPreview || blobUrlPictogram, - message && calculateMediaViewerDimensions(photoDimensions!, hasFooter), + message && calculateMediaViewerDimensions(dimensions!, hasFooter), !IS_SINGLE_COLUMN_LAYOUT && !isZoomed, )} {isVideo && ( @@ -440,9 +462,9 @@ const MediaViewer: FC = ({ url={localBlobUrl || fullMediaData} isGif={isGif} posterData={bestImageData} - posterSize={message && calculateMediaViewerDimensions(videoDimensions!, hasFooter, true)} + posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, true)} downloadProgress={downloadProgress} - fileSize={messageVideo!.size} + fileSize={videoSize!} isMediaViewerOpen={isOpen} noPlay={!isActive} onClose={close} @@ -464,12 +486,17 @@ const MediaViewer: FC = ({ } function renderSenderInfo() { - return ( + return isAvatar ? ( + ) : ( + ); } @@ -507,7 +534,7 @@ const MediaViewer: FC = ({ onCloseMediaViewer={close} onForward={handleForward} onZoomToggle={handleZoomToggle} - isAvatar={Boolean(avatarOwner)} + isAvatar={isAvatar} />
= ({ ); }; -function renderPhoto(blobUrl?: string, imageSize?: IDimensions, canDrag?: boolean) { +function renderPhoto(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean) { return blobUrl ? ( = ({ isDownloadStarted, downloadProgress, handleDownloadClick, - } = useMediaDownload(message && isVideo ? getMessageMediaHash(message, 'download') : undefined); + } = useMediaDownload( + message && isVideo ? getMessageMediaHash(message, 'download') : undefined, + fileName, + ); const lang = useLang(); diff --git a/src/components/mediaViewer/VideoPlayer.tsx b/src/components/mediaViewer/VideoPlayer.tsx index 597faf3b5..ed10c8816 100644 --- a/src/components/mediaViewer/VideoPlayer.tsx +++ b/src/components/mediaViewer/VideoPlayer.tsx @@ -2,8 +2,6 @@ import React, { FC, memo, useCallback, useEffect, useRef, useState, } from '../../lib/teact/teact'; -import { IDimensions } from '../../modules/helpers'; - import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment'; import useShowTransition from '../../hooks/useShowTransition'; import useBuffering from '../../hooks/useBuffering'; @@ -15,12 +13,13 @@ import VideoPlayerControls from './VideoPlayerControls'; import ProgressSpinner from '../ui/ProgressSpinner'; import './VideoPlayer.scss'; +import { ApiDimensions } from '../../api/types'; type OwnProps = { url?: string; isGif?: boolean; posterData?: string; - posterSize?: IDimensions; + posterSize?: ApiDimensions; downloadProgress?: number; fileSize: number; isMediaViewerOpen?: boolean; diff --git a/src/components/mediaViewer/helpers/ghostAnimation.ts b/src/components/mediaViewer/helpers/ghostAnimation.ts index 86bc72887..cc0cab036 100644 --- a/src/components/mediaViewer/helpers/ghostAnimation.ts +++ b/src/components/mediaViewer/helpers/ghostAnimation.ts @@ -1,28 +1,25 @@ -import { ApiMessage } from '../../../api/types'; +import { ApiMessage, ApiDimensions } from '../../../api/types'; + import { MediaViewerOrigin } from '../../../types'; import { ANIMATION_END_DELAY } from '../../../config'; import { - getMessageContent, - getMessageWebPagePhoto, - getMessageWebPageVideo, - getPhotoFullDimensions, - getVideoDimensions, -} from '../../../modules/helpers'; -import { - AVATAR_FULL_DIMENSIONS, calculateDimensions, getMediaViewerAvailableDimensions, MEDIA_VIEWER_MEDIA_QUERY, REM, } from '../../common/helpers/mediaDimensions'; - import windowSize from '../../../util/windowSize'; const ANIMATION_DURATION = 200; export function animateOpening( - hasFooter: boolean, origin: MediaViewerOrigin, bestImageData: string, message?: ApiMessage, + hasFooter: boolean, + origin: MediaViewerOrigin, + bestImageData: string, + dimensions: ApiDimensions, + isVideo: boolean, + message?: ApiMessage, ) { const { mediaEl: fromImage } = getNodes(origin, message); if (!fromImage) { @@ -30,27 +27,11 @@ export function animateOpening( } const { width: windowWidth } = windowSize.get(); - - let isVideo = false; - let mediaSize; - if (message) { - const { photo, video } = getMessageContent(message); - const webPagePhoto = getMessageWebPagePhoto(message); - const webPageVideo = getMessageWebPageVideo(message); - isVideo = Boolean(video || webPageVideo); - mediaSize = isVideo - ? getVideoDimensions((video || webPageVideo)!)! - : getPhotoFullDimensions((photo || webPagePhoto)!)!; - } else { - mediaSize = AVATAR_FULL_DIMENSIONS; - } - - // eslint-disable-next-line max-len const { width: availableWidth, height: availableHeight, } = getMediaViewerAvailableDimensions(hasFooter, isVideo); const { width: toWidth, height: toHeight } = calculateDimensions( - availableWidth, availableHeight, mediaSize.width, mediaSize.height, + availableWidth, availableHeight, dimensions.width, dimensions.height, ); const toLeft = (windowWidth - toWidth) / 2; const toTop = getTopOffset(hasFooter) + (availableHeight - toHeight) / 2; diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 31053a005..01b2a541a 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -639,6 +639,7 @@ const Message: FC = ({ uploadProgress={uploadProgress} isSelectable={isInDocumentGroup} isSelected={isSelected} + onMediaClick={handleMediaClick} onCancelUpload={handleCancelUpload} /> )} diff --git a/src/components/middle/message/helpers/calculateAlbumLayout.ts b/src/components/middle/message/helpers/calculateAlbumLayout.ts index 5a5996afa..9acdaeab9 100644 --- a/src/components/middle/message/helpers/calculateAlbumLayout.ts +++ b/src/components/middle/message/helpers/calculateAlbumLayout.ts @@ -4,8 +4,7 @@ // https://github.com/overtake/TelegramSwift/blob/master/Telegram-Mac/GroupedLayout.swift#L83 import { IAlbum } from '../../../../types'; -import { ApiMessage } from '../../../../api/types'; -import { IDimensions } from '../../../../modules/helpers'; +import { ApiMessage, ApiDimensions } from '../../../../api/types'; import { getAvailableWidth, REM } from '../../../common/helpers/mediaDimensions'; import { calculateMediaDimensions } from './mediaDimensions'; @@ -43,13 +42,13 @@ type ILayoutParams = { }; export type IAlbumLayout = { layout: IMediaLayout[]; - containerStyle: IDimensions; + containerStyle: ApiDimensions; }; function getRatios(messages: ApiMessage[]) { return messages.map( (message) => { - const dimensions = calculateMediaDimensions(message) as IDimensions; + const dimensions = calculateMediaDimensions(message) as ApiDimensions; return dimensions.width / dimensions.height; }, @@ -77,7 +76,7 @@ function cropRatios(ratios: number[], averageRatio: number) { } function calculateContainerSize(layout: IMediaLayout[]) { - const styles: IDimensions = { width: 0, height: 0 }; + const styles: ApiDimensions = { width: 0, height: 0 }; layout.forEach(({ dimensions, sides, diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts index 1b6c2c813..cd08256e9 100644 --- a/src/hooks/useWindowSize.ts +++ b/src/hooks/useWindowSize.ts @@ -1,14 +1,13 @@ import { useEffect, useState } from '../lib/teact/teact'; -import { IDimensions } from '../modules/helpers'; - import { throttle } from '../util/schedulers'; import windowSize from '../util/windowSize'; +import { ApiDimensions } from '../api/types'; const THROTTLE = 250; export default () => { - const [size, setSize] = useState(windowSize.get()); + const [size, setSize] = useState(windowSize.get()); useEffect(() => { const handleResize = throttle(() => { diff --git a/src/modules/helpers/messageMedia.ts b/src/modules/helpers/messageMedia.ts index 4ecf865f6..a026fdb1d 100644 --- a/src/modules/helpers/messageMedia.ts +++ b/src/modules/helpers/messageMedia.ts @@ -1,17 +1,18 @@ import { - ApiAudio, ApiMediaFormat, ApiMessage, ApiMessageSearchType, ApiPhoto, ApiVideo, + ApiAudio, ApiMediaFormat, ApiMessage, ApiMessageSearchType, ApiPhoto, ApiVideo, ApiDimensions, } from '../../api/types'; import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, IS_SAFARI } from '../../util/environment'; import { getMessageKey, isMessageLocal, matchLinkInMessageText } from './messages'; import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo'; -export type IDimensions = { - width: number; - height: number; -}; - -type Target = 'micro' | 'pictogram' | 'inline' | 'viewerPreview' | 'viewerFull' | 'download'; +type Target = + 'micro' + | 'pictogram' + | 'inline' + | 'viewerPreview' + | 'viewerFull' + | 'download'; export function getMessageContent(message: ApiMessage) { @@ -66,6 +67,16 @@ export function getMessageDocument(message: ApiMessage) { return message.content.document; } +export function isMessageDocumentPhoto(message: ApiMessage) { + const document = getMessageDocument(message); + return document ? document.mediaType === 'photo' : undefined; +} + +export function isMessageDocumentVideo(message: ApiMessage) { + const document = getMessageDocument(message); + return document ? document.mediaType === 'video' : undefined; +} + export function getMessageContact(message: ApiMessage) { return message.content.contact; } @@ -177,6 +188,7 @@ export function getMessageMediaHash( case 'micro': case 'pictogram': case 'inline': + case 'viewerPreview': if (!getDocumentHasPreview(document) || hasMessageLocalBlobUrl(message)) { return undefined; } @@ -262,8 +274,10 @@ export function getMessageMediaFormat( return ApiMediaFormat.BlobUrl; } -export function getMessageMediaFilename(message: ApiMessage) { - const { photo, video } = message.content; +export function getMessageFileName(message: ApiMessage) { + const { + photo, video, document, + } = message.content; const webPagePhoto = getMessageWebPagePhoto(message); const webPageVideo = getMessageWebPageVideo(message); @@ -271,15 +285,17 @@ export function getMessageMediaFilename(message: ApiMessage) { return `photo${message.date}.jpeg`; } - if (webPageVideo) { - return webPageVideo.fileName; - } + const { fileName } = video || webPageVideo || document || {}; - if (video) { - return video.fileName; - } + return fileName; +} - return undefined; +export function getMessageFileSize(message: ApiMessage) { + const { video, document } = message.content; + const webPageVideo = getMessageWebPageVideo(message); + const { size } = video || webPageVideo || document || {}; + + return size; } export function hasMessageLocalBlobUrl(message: ApiMessage) { @@ -289,14 +305,14 @@ export function hasMessageLocalBlobUrl(message: ApiMessage) { } export function getChatMediaMessageIds( - messages: Record, listedIds: number[], reverseOrder = false, + messages: Record, listedIds: number[], isFromSharedMedia = false, ) { - const ids = getMessageContentIds(messages, listedIds, 'media'); + const ids = getMessageContentIds(messages, listedIds, isFromSharedMedia ? 'media' : 'inlineMedia'); - return reverseOrder ? ids.reverse() : ids; + return isFromSharedMedia ? ids.reverse() : ids; } -export function getPhotoFullDimensions(photo: ApiPhoto): IDimensions | undefined { +export function getPhotoFullDimensions(photo: ApiPhoto): ApiDimensions | undefined { return ( photo.sizes.find((size) => size.type === 'z') || photo.sizes.find((size) => size.type === 'y') @@ -304,7 +320,7 @@ export function getPhotoFullDimensions(photo: ApiPhoto): IDimensions | undefined ); } -export function getPhotoInlineDimensions(photo: ApiPhoto): IDimensions | undefined { +export function getPhotoInlineDimensions(photo: ApiPhoto): ApiDimensions | undefined { return ( photo.sizes.find((size) => size.type === 'x') || photo.sizes.find((size) => size.type === 'm') @@ -313,9 +329,9 @@ export function getPhotoInlineDimensions(photo: ApiPhoto): IDimensions | undefin ); } -export function getVideoDimensions(video: ApiVideo): IDimensions | undefined { +export function getVideoDimensions(video: ApiVideo): ApiDimensions | undefined { if (video.width && video.height) { - return video as IDimensions; + return video as ApiDimensions; } return undefined; @@ -332,7 +348,7 @@ export function getMediaTransferState(message: ApiMessage, progress?: number, is } export function getMessageContentIds( - messages: Record, messageIds: number[], contentType: ApiMessageSearchType, + messages: Record, messageIds: number[], contentType: ApiMessageSearchType | 'inlineMedia', ) { let validator: Function; @@ -356,6 +372,18 @@ export function getMessageContentIds( validator = getMessageAudio; break; + case 'inlineMedia': + validator = (message: ApiMessage) => { + const video = getMessageVideo(message); + return ( + getMessagePhoto(message) + || (video && !video.isRound && !video.isGif) + || isMessageDocumentPhoto(message) + || isMessageDocumentVideo(message) + ); + }; + break; + default: return [] as Array; } diff --git a/src/types/index.ts b/src/types/index.ts index b24af66c0..9d12f3ec9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ import { - ApiLanguage, ApiMessage, ApiStickerSet, ApiShippingAddress, + ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet, } from '../api/types'; export enum LoadMoreDirection {