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 {
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,
};
}

View File

@ -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 {

View File

@ -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<OwnProps> = ({
isSelected,
isSelectable,
onCancelUpload,
onMediaClick,
onDateClick,
}) => {
// eslint-disable-next-line no-null/no-null
@ -48,6 +55,7 @@ const Document: FC<OwnProps> = ({
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<OwnProps> = ({
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<OwnProps> = ({
sender={sender}
isSelectable={isSelectable}
isSelected={isSelected}
actionIcon={withMediaViewer ? (isMessageDocumentVideo(message) ? 'icon-play' : 'icon-eye') : 'icon-download'}
onClick={handleClick}
onDateClick={onDateClick ? handleDateClick : undefined}
/>

View File

@ -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;
}

View File

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

View File

@ -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);

View File

@ -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<StateProps & DispatchProps> = ({
focusMessage,
animationLevel,
}) => {
// eslint-disable-next-line no-null/no-null
const animationKey = useRef<number>(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<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(() => {
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<number>();
const prevSenderId = usePrevious<number | undefined>(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<boolean>(false);
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]
? `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<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
const lang = useLang();
function renderSlide(isActive: boolean) {
if (avatarOwner) {
if (isAvatar) {
return (
<div key={chatId} className="media-viewer-content">
{renderPhoto(
@ -431,7 +453,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
>
{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<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
}
function renderSenderInfo() {
return (
return isAvatar ? (
<SenderInfo
key={avatarOwner ? avatarOwner.id : messageId}
chatId={avatarOwner ? avatarOwner.id : chatId}
key={avatarOwner!.id}
chatId={avatarOwner!.id}
isAvatar
/>
) : (
<SenderInfo
key={messageId}
chatId={chatId}
messageId={messageId}
isAvatar={Boolean(avatarOwner)}
/>
);
}
@ -507,7 +534,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
onCloseMediaViewer={close}
onForward={handleForward}
onZoomToggle={handleZoomToggle}
isAvatar={Boolean(avatarOwner)}
isAvatar={isAvatar}
/>
</div>
<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
? (
<img

View File

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

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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,

View File

@ -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<IDimensions>(windowSize.get());
const [size, setSize] = useState<ApiDimensions>(windowSize.get());
useEffect(() => {
const handleResize = throttle(() => {

View File

@ -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<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 (
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<number, ApiMessage>, messageIds: number[], contentType: ApiMessageSearchType,
messages: Record<number, ApiMessage>, 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<number>;
}

View File

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