mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-22 05:11:55 +01:00
Media Viewer: Support document preview (#1247)
This commit is contained in:
parent
9b522ff50b
commit
265c710d19
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -639,6 +639,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
uploadProgress={uploadProgress}
|
||||
isSelectable={isInDocumentGroup}
|
||||
isSelected={isSelected}
|
||||
onMediaClick={handleMediaClick}
|
||||
onCancelUpload={handleCancelUpload}
|
||||
/>
|
||||
)}
|
||||
|
@ -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,
|
||||
|
@ -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(() => {
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {
|
||||
ApiLanguage, ApiMessage, ApiStickerSet, ApiShippingAddress,
|
||||
ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet,
|
||||
} from '../api/types';
|
||||
|
||||
export enum LoadMoreDirection {
|
||||
|
Loading…
x
Reference in New Issue
Block a user