Media Viewer: Support document preview (#1247)

This commit is contained in:
Alexander Zinchuk 2021-07-07 18:38:39 +03:00
parent 9b522ff50b
commit 265c710d19
15 changed files with 243 additions and 150 deletions

View File

@ -395,11 +395,38 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u
} }
const { const {
id, size, mimeType, date, thumbs, id, size, mimeType, date, thumbs, attributes,
} = document; } = document;
const thumbnail = thumbs && buildApiThumbnailFromStripped(thumbs); 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 { return {
id: String(id), id: String(id),
size, size,
@ -407,6 +434,8 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u
timestamp: date, timestamp: date,
fileName: getFilenameFromDocument(document), fileName: getFilenameFromDocument(document),
thumbnail, thumbnail,
mediaType,
mediaSize,
}; };
} }

View File

@ -1,13 +1,14 @@
export interface ApiPhotoSize { export interface ApiDimensions {
type: 's' | 'm' | 'x' | 'y' | 'z';
width: number; width: number;
height: number; height: number;
} }
export interface ApiThumbnail { export interface ApiPhotoSize extends ApiDimensions {
type: 's' | 'm' | 'x' | 'y' | 'z';
}
export interface ApiThumbnail extends ApiDimensions {
dataUri: string; dataUri: string;
height: number;
width: number;
} }
export interface ApiPhoto { export interface ApiPhoto {
@ -81,6 +82,8 @@ export interface ApiDocument {
mimeType: string; mimeType: string;
thumbnail?: ApiThumbnail; thumbnail?: ApiThumbnail;
previewBlobUrl?: string; previewBlobUrl?: string;
mediaType?: 'photo' | 'video';
mediaSize?: ApiDimensions;
} }
export interface ApiContact { export interface ApiContact {

View File

@ -5,7 +5,12 @@ import React, {
import { ApiMessage } from '../../api/types'; import { ApiMessage } from '../../api/types';
import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo'; 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 { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgress'; import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgress';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
@ -25,6 +30,7 @@ type OwnProps = {
className?: string; className?: string;
sender?: string; sender?: string;
onCancelUpload?: () => void; onCancelUpload?: () => void;
onMediaClick?: () => void;
onDateClick?: (messageId: number, chatId: number) => void; onDateClick?: (messageId: number, chatId: number) => void;
}; };
@ -40,6 +46,7 @@ const Document: FC<OwnProps> = ({
isSelected, isSelected,
isSelectable, isSelectable,
onCancelUpload, onCancelUpload,
onMediaClick,
onDateClick, onDateClick,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -48,6 +55,7 @@ const Document: FC<OwnProps> = ({
const document = message.content.document!; const document = message.content.document!;
const extension = getDocumentExtension(document) || ''; const extension = getDocumentExtension(document) || '';
const { fileName, size, timestamp } = document; const { fileName, size, timestamp } = document;
const withMediaViewer = onMediaClick && Boolean(document.mediaType);
const isIntersecting = useIsIntersecting(ref, observeIntersection); const isIntersecting = useIsIntersecting(ref, observeIntersection);
@ -65,14 +73,16 @@ const Document: FC<OwnProps> = ({
const previewData = useMedia(getMessageMediaHash(message, 'pictogram'), !isIntersecting); const previewData = useMedia(getMessageMediaHash(message, 'pictogram'), !isIntersecting);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (isUploading) { if (withMediaViewer) {
onMediaClick!();
} else if (isUploading) {
if (onCancelUpload) { if (onCancelUpload) {
onCancelUpload(); onCancelUpload();
} }
} else { } else {
setIsDownloadAllowed((isAllowed) => !isAllowed); setIsDownloadAllowed((isAllowed) => !isAllowed);
} }
}, [isUploading, onCancelUpload]); }, [withMediaViewer, isUploading, onCancelUpload, onMediaClick]);
const handleDateClick = useCallback(() => { const handleDateClick = useCallback(() => {
onDateClick!(message.id, message.chatId); onDateClick!(message.id, message.chatId);
@ -102,6 +112,7 @@ const Document: FC<OwnProps> = ({
sender={sender} sender={sender}
isSelectable={isSelectable} isSelectable={isSelectable}
isSelected={isSelected} isSelected={isSelected}
actionIcon={withMediaViewer ? (isMessageDocumentVideo(message) ? 'icon-play' : 'icon-eye') : 'icon-download'}
onClick={handleClick} onClick={handleClick}
onDateClick={onDateClick ? handleDateClick : undefined} onDateClick={onDateClick ? handleDateClick : undefined}
/> />

View File

@ -70,7 +70,7 @@
background: transparent; background: transparent;
overflow: hidden; overflow: hidden;
& + .icon-download, & + .action-icon,
& + .file-progress { & + .file-progress {
background: rgba(black, 0.5); background: rgba(black, 0.5);
border-radius: var(--border-radius-messages-small); border-radius: var(--border-radius-messages-small);
@ -81,7 +81,7 @@
} }
} }
.icon-download { .action-icon {
color: #fff; color: #fff;
font-size: 1.5rem; font-size: 1.5rem;
position: absolute; position: absolute;
@ -104,7 +104,7 @@
border-width: 0; border-width: 0;
} }
.icon-download { .action-icon {
opacity: 1; opacity: 1;
&.hidden { &.hidden {
@ -164,7 +164,7 @@
--background-color: var(--color-background); --background-color: var(--color-background);
--border-radius-messages-small: .3125rem; --border-radius-messages-small: .3125rem;
.icon-download, .action-icon,
.file-progress, .file-progress,
.file-icon, .file-icon,
.file-preview { .file-preview {
@ -211,7 +211,7 @@
} }
} }
.icon-download { .action-icon {
left: auto; left: auto;
right: 0; right: 0;
} }

View File

@ -31,6 +31,7 @@ type OwnProps = {
isSelectable?: boolean; isSelectable?: boolean;
isSelected?: boolean; isSelected?: boolean;
transferProgress?: number; transferProgress?: number;
actionIcon?: string;
onClick?: () => void; onClick?: () => void;
onDateClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void; onDateClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
}; };
@ -51,6 +52,7 @@ const File: FC<OwnProps> = ({
isSelectable, isSelectable,
isSelected, isSelected,
transferProgress, transferProgress,
actionIcon,
onClick, onClick,
onDateClick, onDateClick,
}) => { }) => {
@ -126,7 +128,15 @@ const File: FC<OwnProps> = ({
/> />
</div> </div>
)} )}
{onClick && <i className={buildClassName('icon-download', shouldSpinnerRender && 'hidden')} />} {onClick && (
<i
className={buildClassName(
'action-icon',
actionIcon || 'icon-download',
shouldSpinnerRender && 'hidden',
)}
/>
)}
</div> </div>
<div className="file-info"> <div className="file-info">
<div className="file-title" dir="auto">{renderText(name)}</div> <div className="file-title" dir="auto">{renderText(name)}</div>

View File

@ -1,16 +1,19 @@
import { ApiPhoto, ApiVideo, ApiSticker } from '../../../api/types'; import {
import { getPhotoInlineDimensions, getVideoDimensions, IDimensions } from '../../../modules/helpers'; ApiPhoto, ApiVideo, ApiSticker, ApiDimensions,
import windowSize from '../../../util/windowSize'; } from '../../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { STICKER_SIZE_INLINE_DESKTOP_FACTOR, STICKER_SIZE_INLINE_MOBILE_FACTOR } from '../../../config'; 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 MEDIA_VIEWER_MEDIA_QUERY = '(max-height: 640px)';
export const REM = parseInt(getComputedStyle(document.documentElement).fontSize, 10); export const REM = parseInt(getComputedStyle(document.documentElement).fontSize, 10);
export const ROUND_VIDEO_DIMENSIONS = 200; export const ROUND_VIDEO_DIMENSIONS = 200;
export const AVATAR_FULL_DIMENSIONS = { width: 640, height: 640 }; export const AVATAR_FULL_DIMENSIONS = { width: 640, height: 640 };
const DEFAULT_MEDIA_DIMENSIONS: IDimensions = { width: 100, height: 100 };
export const LIKE_STICKER_ID = '1258816259753933'; 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_NO_AVATARS_MESSAGE_EXTRA_WIDTH_REM = 4.5;
const MOBILE_SCREEN_MESSAGE_EXTRA_WIDTH_REM = 7; const MOBILE_SCREEN_MESSAGE_EXTRA_WIDTH_REM = 7;
const MESSAGE_MAX_WIDTH_REM = 29; const MESSAGE_MAX_WIDTH_REM = 29;
@ -92,7 +95,7 @@ function calculateDimensionsForMessageMedia({
isWebPagePhoto?: boolean; isWebPagePhoto?: boolean;
isGif?: boolean; isGif?: boolean;
noAvatars?: boolean; noAvatars?: boolean;
}): IDimensions { }): ApiDimensions {
const aspectRatio = height / width; const aspectRatio = height / width;
const availableWidth = getAvailableWidth(fromOwnMessage, isForwarded, isWebPagePhoto, noAvatars); const availableWidth = getAvailableWidth(fromOwnMessage, isForwarded, isWebPagePhoto, noAvatars);
const availableHeight = getAvailableHeight(isGif, aspectRatio); const availableHeight = getAvailableHeight(isGif, aspectRatio);
@ -100,7 +103,7 @@ function calculateDimensionsForMessageMedia({
return calculateDimensions(availableWidth, availableHeight, width, height); 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 mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
const { width: windowWidth, height: windowHeight } = windowSize.get(); const { width: windowWidth, height: windowHeight } = windowSize.get();
let occupiedHeightRem = isVideo && mql.matches ? 10 : 8.25; let occupiedHeightRem = isVideo && mql.matches ? 10 : 8.25;
@ -151,14 +154,14 @@ export function calculateVideoDimensions(
}); });
} }
export function getPictogramDimensions(): IDimensions { export function getPictogramDimensions(): ApiDimensions {
return { return {
width: 2 * REM, width: 2 * REM,
height: 2 * REM, height: 2 * REM,
}; };
} }
export function getDocumentThumbnailDimensions(smaller?: boolean): IDimensions { export function getDocumentThumbnailDimensions(smaller?: boolean): ApiDimensions {
if (smaller) { if (smaller) {
return { return {
width: 3 * REM, 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; const { width } = sticker;
let { height } = sticker; let { height } = sticker;
@ -203,8 +206,8 @@ export function getStickerDimensions(sticker: ApiSticker): IDimensions {
} }
export function calculateMediaViewerDimensions( export function calculateMediaViewerDimensions(
{ width, height }: IDimensions, withFooter: boolean, isVideo: boolean = false, { width, height }: ApiDimensions, withFooter: boolean, isVideo: boolean = false,
): IDimensions { ): ApiDimensions {
const { width: availableWidth, height: availableHeight } = getMediaViewerAvailableDimensions(withFooter, isVideo); const { width: availableWidth, height: availableHeight } = getMediaViewerAvailableDimensions(withFooter, isVideo);
return calculateDimensions(availableWidth, availableHeight, width, height); return calculateDimensions(availableWidth, availableHeight, width, height);
@ -215,7 +218,7 @@ export function calculateDimensions(
availableHeight: number, availableHeight: number,
mediaWidth: number, mediaWidth: number,
mediaHeight: number, mediaHeight: number,
): IDimensions { ): ApiDimensions {
const aspectRatio = mediaHeight / mediaWidth; const aspectRatio = mediaHeight / mediaWidth;
const calculatedWidth = Math.min(mediaWidth, availableWidth); const calculatedWidth = Math.min(mediaWidth, availableWidth);
const calculatedHeight = Math.round(calculatedWidth * aspectRatio); const calculatedHeight = Math.round(calculatedWidth * aspectRatio);

View File

@ -5,7 +5,7 @@ import { withGlobal } from '../../lib/teact/teactn';
import { GlobalActions } from '../../global/types'; import { GlobalActions } from '../../global/types';
import { import {
ApiChat, ApiMediaFormat, ApiMessage, ApiUser, ApiChat, ApiMediaFormat, ApiMessage, ApiUser, ApiDimensions,
} from '../../api/types'; } from '../../api/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
@ -30,17 +30,20 @@ import {
import { import {
getChatAvatarHash, getChatAvatarHash,
getChatMediaMessageIds, getChatMediaMessageIds,
getMessageMediaFilename, getMessageFileName,
getMessageMediaFormat, getMessageMediaFormat,
getMessageMediaHash, getMessageMediaHash,
getMessageMediaThumbDataUri, getMessageMediaThumbDataUri,
getMessagePhoto, getMessagePhoto,
getMessageVideo, getMessageVideo,
getMessageDocument,
isMessageDocumentPhoto,
isMessageDocumentVideo,
getMessageWebPagePhoto, getMessageWebPagePhoto,
getMessageWebPageVideo, getMessageWebPageVideo,
getPhotoFullDimensions, getPhotoFullDimensions,
getVideoDimensions, getVideoDimensions, getMessageFileSize,
IDimensions,
} from '../../modules/helpers'; } from '../../modules/helpers';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
import { captureEvents, SwipeDirection } from '../../util/captureEvents'; import { captureEvents, SwipeDirection } from '../../util/captureEvents';
@ -104,56 +107,62 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
focusMessage, focusMessage,
animationLevel, animationLevel,
}) => { }) => {
// eslint-disable-next-line no-null/no-null
const animationKey = useRef<number>(null);
const isOpen = Boolean(avatarOwner || messageId); 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 isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia;
const isFromSearch = origin === MediaViewerOrigin.SearchResult; 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<number | undefined>(senderId);
const [canPanZoomWrap, setCanPanZoomWrap] = useState(false);
const [isZoomed, setIsZoomed] = useState<boolean>(false);
const [zoomLevel, setZoomLevel] = useState<number>(1);
const [panDelta, setPanDelta] = useState({ x: 0, y: 0 });
const [isFooterHidden, setIsFooterHidden] = useState<boolean>(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(() => { const messageIds = useMemo(() => {
return (isWebPagePhoto || isWebPageVideo) && messageId return isSingleSlide && messageId
? [messageId] ? [messageId]
: getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia); : getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia);
}, [isWebPagePhoto, isWebPageVideo, messageId, chatMessages, collectionIds, isFromSharedMedia]); }, [isSingleSlide, messageId, chatMessages, collectionIds, isFromSharedMedia]);
const selectedMediaMessageIndex = messageId ? messageIds.indexOf(messageId) : -1; const selectedMediaMessageIndex = messageId ? messageIds.indexOf(messageId) : -1;
const isFirst = selectedMediaMessageIndex === 0 || selectedMediaMessageIndex === -1; const isFirst = selectedMediaMessageIndex === 0 || selectedMediaMessageIndex === -1;
const isLast = selectedMediaMessageIndex === messageIds.length - 1 || selectedMediaMessageIndex === -1; const isLast = selectedMediaMessageIndex === messageIds.length - 1 || selectedMediaMessageIndex === -1;
/* Animation */
const animationKey = useRef<number>();
const prevSenderId = usePrevious<number | undefined>(senderId);
if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) { if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) {
animationKey.current = selectedMediaMessageIndex; 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) { /* Controls */
if (avatarOwner && profilePhotoIndex !== undefined) { const [isFooterHidden, setIsFooterHidden] = useState<boolean>(false);
const { photos } = avatarOwner; const [canPanZoomWrap, setCanPanZoomWrap] = useState(false);
const [isZoomed, setIsZoomed] = useState<boolean>(false);
const [zoomLevel, setZoomLevel] = useState<number>(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] return photos && photos[profilePhotoIndex]
? `photo${photos[profilePhotoIndex].id}?size=c` ? `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( const blobUrlPictogram = useMedia(
@ -167,7 +176,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
const blobUrlPreview = useMedia( const blobUrlPreview = useMedia(
previewMediaHash, previewMediaHash,
undefined, undefined,
avatarOwner && previewMediaHash && previewMediaHash.startsWith('profilePhoto') isAvatar && previewMediaHash && previewMediaHash.startsWith('profilePhoto')
? ApiMediaFormat.DataUri ? ApiMediaFormat.DataUri
: ApiMediaFormat.BlobUrl, : ApiMediaFormat.BlobUrl,
undefined, undefined,
@ -188,12 +197,25 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
bestImageData = thumbDataUri; bestImageData = thumbDataUri;
} }
const photoDimensions = isPhoto ? getPhotoFullDimensions(( const videoSize = message ? getMessageFileSize(message) : undefined;
isWebPagePhoto ? webPagePhoto : photo const fileName = message
)!) : undefined; ? getMessageFileName(message)
const videoDimensions = isVideo ? getVideoDimensions(( : isAvatar
isWebPageVideo ? webPageVideo : video ? `avatar${avatarOwner!.id}-${profilePhotoIndex}.jpg`
)!) : undefined; : 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(() => { useEffect(() => {
if (!IS_SINGLE_COLUMN_LAYOUT) { if (!IS_SINGLE_COLUMN_LAYOUT) {
@ -230,7 +252,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY); dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
const textParts = message ? renderMessageText(message) : undefined; const textParts = message ? renderMessageText(message) : undefined;
const hasFooter = Boolean(textParts); const hasFooter = Boolean(textParts);
animateOpening(hasFooter, origin!, bestImageData!, message); animateOpening(hasFooter, origin!, bestImageData!, dimensions, isVideo, message);
} }
if (isGhostAnimation && !isOpen && (prevMessage || prevAvatarOwner)) { if (isGhostAnimation && !isOpen && (prevMessage || prevAvatarOwner)) {
@ -238,8 +260,8 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
animateClosing(prevOrigin!, prevBestImageData!, prevMessage || undefined); animateClosing(prevOrigin!, prevBestImageData!, prevMessage || undefined);
} }
}, [ }, [
isGhostAnimation, isOpen, origin, prevOrigin, isGhostAnimation, isOpen, origin, prevOrigin, message, prevMessage, prevAvatarOwner,
message, prevMessage, prevAvatarOwner, bestImageData, prevBestImageData, bestImageData, prevBestImageData, dimensions, isVideo,
]); ]);
useEffect(() => { useEffect(() => {
@ -409,7 +431,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
const lang = useLang(); const lang = useLang();
function renderSlide(isActive: boolean) { function renderSlide(isActive: boolean) {
if (avatarOwner) { if (isAvatar) {
return ( return (
<div key={chatId} className="media-viewer-content"> <div key={chatId} className="media-viewer-content">
{renderPhoto( {renderPhoto(
@ -431,7 +453,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
> >
{isPhoto && renderPhoto( {isPhoto && renderPhoto(
localBlobUrl || fullMediaData || blobUrlPreview || blobUrlPictogram, localBlobUrl || fullMediaData || blobUrlPreview || blobUrlPictogram,
message && calculateMediaViewerDimensions(photoDimensions!, hasFooter), message && calculateMediaViewerDimensions(dimensions!, hasFooter),
!IS_SINGLE_COLUMN_LAYOUT && !isZoomed, !IS_SINGLE_COLUMN_LAYOUT && !isZoomed,
)} )}
{isVideo && ( {isVideo && (
@ -440,9 +462,9 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
url={localBlobUrl || fullMediaData} url={localBlobUrl || fullMediaData}
isGif={isGif} isGif={isGif}
posterData={bestImageData} posterData={bestImageData}
posterSize={message && calculateMediaViewerDimensions(videoDimensions!, hasFooter, true)} posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, true)}
downloadProgress={downloadProgress} downloadProgress={downloadProgress}
fileSize={messageVideo!.size} fileSize={videoSize!}
isMediaViewerOpen={isOpen} isMediaViewerOpen={isOpen}
noPlay={!isActive} noPlay={!isActive}
onClose={close} onClose={close}
@ -464,12 +486,17 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
} }
function renderSenderInfo() { function renderSenderInfo() {
return ( return isAvatar ? (
<SenderInfo <SenderInfo
key={avatarOwner ? avatarOwner.id : messageId} key={avatarOwner!.id}
chatId={avatarOwner ? avatarOwner.id : chatId} chatId={avatarOwner!.id}
isAvatar
/>
) : (
<SenderInfo
key={messageId}
chatId={chatId}
messageId={messageId} messageId={messageId}
isAvatar={Boolean(avatarOwner)}
/> />
); );
} }
@ -507,7 +534,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
onCloseMediaViewer={close} onCloseMediaViewer={close}
onForward={handleForward} onForward={handleForward}
onZoomToggle={handleZoomToggle} onZoomToggle={handleZoomToggle}
isAvatar={Boolean(avatarOwner)} isAvatar={isAvatar}
/> />
</div> </div>
<PanZoom <PanZoom
@ -554,7 +581,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
); );
}; };
function renderPhoto(blobUrl?: string, imageSize?: IDimensions, canDrag?: boolean) { function renderPhoto(blobUrl?: string, imageSize?: ApiDimensions, canDrag?: boolean) {
return blobUrl return blobUrl
? ( ? (
<img <img

View File

@ -5,12 +5,12 @@ import { ApiMessage } from '../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { getMessageMediaHash } from '../../modules/helpers'; import { getMessageMediaHash } from '../../modules/helpers';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useMediaDownload from '../../hooks/useMediaDownload';
import Button from '../ui/Button'; import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu'; import DropdownMenu from '../ui/DropdownMenu';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
import ProgressSpinner from '../ui/ProgressSpinner'; import ProgressSpinner from '../ui/ProgressSpinner';
import useMediaDownload from '../../hooks/useMediaDownload';
import './MediaViewerActions.scss'; import './MediaViewerActions.scss';
@ -41,7 +41,10 @@ const MediaViewerActions: FC<OwnProps> = ({
isDownloadStarted, isDownloadStarted,
downloadProgress, downloadProgress,
handleDownloadClick, handleDownloadClick,
} = useMediaDownload(message && isVideo ? getMessageMediaHash(message, 'download') : undefined); } = useMediaDownload(
message && isVideo ? getMessageMediaHash(message, 'download') : undefined,
fileName,
);
const lang = useLang(); const lang = useLang();

View File

@ -2,8 +2,6 @@ import React, {
FC, memo, useCallback, useEffect, useRef, useState, FC, memo, useCallback, useEffect, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { IDimensions } from '../../modules/helpers';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment'; import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useBuffering from '../../hooks/useBuffering'; import useBuffering from '../../hooks/useBuffering';
@ -15,12 +13,13 @@ import VideoPlayerControls from './VideoPlayerControls';
import ProgressSpinner from '../ui/ProgressSpinner'; import ProgressSpinner from '../ui/ProgressSpinner';
import './VideoPlayer.scss'; import './VideoPlayer.scss';
import { ApiDimensions } from '../../api/types';
type OwnProps = { type OwnProps = {
url?: string; url?: string;
isGif?: boolean; isGif?: boolean;
posterData?: string; posterData?: string;
posterSize?: IDimensions; posterSize?: ApiDimensions;
downloadProgress?: number; downloadProgress?: number;
fileSize: number; fileSize: number;
isMediaViewerOpen?: boolean; isMediaViewerOpen?: boolean;

View File

@ -1,28 +1,25 @@
import { ApiMessage } from '../../../api/types'; import { ApiMessage, ApiDimensions } from '../../../api/types';
import { MediaViewerOrigin } from '../../../types'; import { MediaViewerOrigin } from '../../../types';
import { ANIMATION_END_DELAY } from '../../../config'; import { ANIMATION_END_DELAY } from '../../../config';
import { import {
getMessageContent,
getMessageWebPagePhoto,
getMessageWebPageVideo,
getPhotoFullDimensions,
getVideoDimensions,
} from '../../../modules/helpers';
import {
AVATAR_FULL_DIMENSIONS,
calculateDimensions, calculateDimensions,
getMediaViewerAvailableDimensions, getMediaViewerAvailableDimensions,
MEDIA_VIEWER_MEDIA_QUERY, MEDIA_VIEWER_MEDIA_QUERY,
REM, REM,
} from '../../common/helpers/mediaDimensions'; } from '../../common/helpers/mediaDimensions';
import windowSize from '../../../util/windowSize'; import windowSize from '../../../util/windowSize';
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
export function animateOpening( 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); const { mediaEl: fromImage } = getNodes(origin, message);
if (!fromImage) { if (!fromImage) {
@ -30,27 +27,11 @@ export function animateOpening(
} }
const { width: windowWidth } = windowSize.get(); 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 { const {
width: availableWidth, height: availableHeight, width: availableWidth, height: availableHeight,
} = getMediaViewerAvailableDimensions(hasFooter, isVideo); } = getMediaViewerAvailableDimensions(hasFooter, isVideo);
const { width: toWidth, height: toHeight } = calculateDimensions( const { width: toWidth, height: toHeight } = calculateDimensions(
availableWidth, availableHeight, mediaSize.width, mediaSize.height, availableWidth, availableHeight, dimensions.width, dimensions.height,
); );
const toLeft = (windowWidth - toWidth) / 2; const toLeft = (windowWidth - toWidth) / 2;
const toTop = getTopOffset(hasFooter) + (availableHeight - toHeight) / 2; const toTop = getTopOffset(hasFooter) + (availableHeight - toHeight) / 2;

View File

@ -639,6 +639,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
uploadProgress={uploadProgress} uploadProgress={uploadProgress}
isSelectable={isInDocumentGroup} isSelectable={isInDocumentGroup}
isSelected={isSelected} isSelected={isSelected}
onMediaClick={handleMediaClick}
onCancelUpload={handleCancelUpload} onCancelUpload={handleCancelUpload}
/> />
)} )}

View File

@ -4,8 +4,7 @@
// https://github.com/overtake/TelegramSwift/blob/master/Telegram-Mac/GroupedLayout.swift#L83 // https://github.com/overtake/TelegramSwift/blob/master/Telegram-Mac/GroupedLayout.swift#L83
import { IAlbum } from '../../../../types'; import { IAlbum } from '../../../../types';
import { ApiMessage } from '../../../../api/types'; import { ApiMessage, ApiDimensions } from '../../../../api/types';
import { IDimensions } from '../../../../modules/helpers';
import { getAvailableWidth, REM } from '../../../common/helpers/mediaDimensions'; import { getAvailableWidth, REM } from '../../../common/helpers/mediaDimensions';
import { calculateMediaDimensions } from './mediaDimensions'; import { calculateMediaDimensions } from './mediaDimensions';
@ -43,13 +42,13 @@ type ILayoutParams = {
}; };
export type IAlbumLayout = { export type IAlbumLayout = {
layout: IMediaLayout[]; layout: IMediaLayout[];
containerStyle: IDimensions; containerStyle: ApiDimensions;
}; };
function getRatios(messages: ApiMessage[]) { function getRatios(messages: ApiMessage[]) {
return messages.map( return messages.map(
(message) => { (message) => {
const dimensions = calculateMediaDimensions(message) as IDimensions; const dimensions = calculateMediaDimensions(message) as ApiDimensions;
return dimensions.width / dimensions.height; return dimensions.width / dimensions.height;
}, },
@ -77,7 +76,7 @@ function cropRatios(ratios: number[], averageRatio: number) {
} }
function calculateContainerSize(layout: IMediaLayout[]) { function calculateContainerSize(layout: IMediaLayout[]) {
const styles: IDimensions = { width: 0, height: 0 }; const styles: ApiDimensions = { width: 0, height: 0 };
layout.forEach(({ layout.forEach(({
dimensions, dimensions,
sides, sides,

View File

@ -1,14 +1,13 @@
import { useEffect, useState } from '../lib/teact/teact'; import { useEffect, useState } from '../lib/teact/teact';
import { IDimensions } from '../modules/helpers';
import { throttle } from '../util/schedulers'; import { throttle } from '../util/schedulers';
import windowSize from '../util/windowSize'; import windowSize from '../util/windowSize';
import { ApiDimensions } from '../api/types';
const THROTTLE = 250; const THROTTLE = 250;
export default () => { export default () => {
const [size, setSize] = useState<IDimensions>(windowSize.get()); const [size, setSize] = useState<ApiDimensions>(windowSize.get());
useEffect(() => { useEffect(() => {
const handleResize = throttle(() => { const handleResize = throttle(() => {

View File

@ -1,17 +1,18 @@
import { import {
ApiAudio, ApiMediaFormat, ApiMessage, ApiMessageSearchType, ApiPhoto, ApiVideo, ApiAudio, ApiMediaFormat, ApiMessage, ApiMessageSearchType, ApiPhoto, ApiVideo, ApiDimensions,
} from '../../api/types'; } from '../../api/types';
import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, IS_SAFARI } from '../../util/environment'; import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, IS_SAFARI } from '../../util/environment';
import { getMessageKey, isMessageLocal, matchLinkInMessageText } from './messages'; import { getMessageKey, isMessageLocal, matchLinkInMessageText } from './messages';
import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo'; import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo';
export type IDimensions = { type Target =
width: number; 'micro'
height: number; | 'pictogram'
}; | 'inline'
| 'viewerPreview'
type Target = 'micro' | 'pictogram' | 'inline' | 'viewerPreview' | 'viewerFull' | 'download'; | 'viewerFull'
| 'download';
export function getMessageContent(message: ApiMessage) { export function getMessageContent(message: ApiMessage) {
@ -66,6 +67,16 @@ export function getMessageDocument(message: ApiMessage) {
return message.content.document; 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) { export function getMessageContact(message: ApiMessage) {
return message.content.contact; return message.content.contact;
} }
@ -177,6 +188,7 @@ export function getMessageMediaHash(
case 'micro': case 'micro':
case 'pictogram': case 'pictogram':
case 'inline': case 'inline':
case 'viewerPreview':
if (!getDocumentHasPreview(document) || hasMessageLocalBlobUrl(message)) { if (!getDocumentHasPreview(document) || hasMessageLocalBlobUrl(message)) {
return undefined; return undefined;
} }
@ -262,8 +274,10 @@ export function getMessageMediaFormat(
return ApiMediaFormat.BlobUrl; return ApiMediaFormat.BlobUrl;
} }
export function getMessageMediaFilename(message: ApiMessage) { export function getMessageFileName(message: ApiMessage) {
const { photo, video } = message.content; const {
photo, video, document,
} = message.content;
const webPagePhoto = getMessageWebPagePhoto(message); const webPagePhoto = getMessageWebPagePhoto(message);
const webPageVideo = getMessageWebPageVideo(message); const webPageVideo = getMessageWebPageVideo(message);
@ -271,15 +285,17 @@ export function getMessageMediaFilename(message: ApiMessage) {
return `photo${message.date}.jpeg`; return `photo${message.date}.jpeg`;
} }
if (webPageVideo) { const { fileName } = video || webPageVideo || document || {};
return webPageVideo.fileName;
}
if (video) { return fileName;
return video.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) { export function hasMessageLocalBlobUrl(message: ApiMessage) {
@ -289,14 +305,14 @@ export function hasMessageLocalBlobUrl(message: ApiMessage) {
} }
export function getChatMediaMessageIds( export function getChatMediaMessageIds(
messages: Record<number, ApiMessage>, listedIds: number[], reverseOrder = false, messages: Record<number, ApiMessage>, 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 ( return (
photo.sizes.find((size) => size.type === 'z') photo.sizes.find((size) => size.type === 'z')
|| photo.sizes.find((size) => size.type === 'y') || 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 ( return (
photo.sizes.find((size) => size.type === 'x') photo.sizes.find((size) => size.type === 'x')
|| photo.sizes.find((size) => size.type === 'm') || 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) { if (video.width && video.height) {
return video as IDimensions; return video as ApiDimensions;
} }
return undefined; return undefined;
@ -332,7 +348,7 @@ export function getMediaTransferState(message: ApiMessage, progress?: number, is
} }
export function getMessageContentIds( export function getMessageContentIds(
messages: Record<number, ApiMessage>, messageIds: number[], contentType: ApiMessageSearchType, messages: Record<number, ApiMessage>, messageIds: number[], contentType: ApiMessageSearchType | 'inlineMedia',
) { ) {
let validator: Function; let validator: Function;
@ -356,6 +372,18 @@ export function getMessageContentIds(
validator = getMessageAudio; validator = getMessageAudio;
break; break;
case 'inlineMedia':
validator = (message: ApiMessage) => {
const video = getMessageVideo(message);
return (
getMessagePhoto(message)
|| (video && !video.isRound && !video.isGif)
|| isMessageDocumentPhoto(message)
|| isMessageDocumentVideo(message)
);
};
break;
default: default:
return [] as Array<number>; return [] as Array<number>;
} }

View File

@ -1,5 +1,5 @@
import { import {
ApiLanguage, ApiMessage, ApiStickerSet, ApiShippingAddress, ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet,
} from '../api/types'; } from '../api/types';
export enum LoadMoreDirection { export enum LoadMoreDirection {