From 5facc1e705374f2881b08be32f261aea7c32c12f Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 6 Jul 2021 19:12:42 +0300 Subject: [PATCH] Message: Support web page videos; Always play videos inline (#1233) --- src/api/gramjs/apiBuilders/messages.ts | 23 +++--- src/api/gramjs/gramjsBuilders/index.ts | 7 +- src/api/gramjs/helpers.ts | 9 +++ src/api/gramjs/methods/media.ts | 11 +++ src/api/types/messages.ts | 5 +- src/components/common/WebLink.scss | 4 +- src/components/common/WebLink.tsx | 3 +- src/components/mediaViewer/MediaViewer.tsx | 22 +++--- .../mediaViewer/helpers/ghostAnimation.ts | 18 +++-- .../middle/composer/WebPagePreview.scss | 4 ++ src/components/middle/message/Message.tsx | 2 + src/components/middle/message/Video.tsx | 38 ++++------ src/components/middle/message/WebPage.scss | 5 ++ src/components/middle/message/WebPage.tsx | 29 ++++++-- .../middle/message/_message-content.scss | 2 +- .../message/helpers/buildContentClassName.ts | 2 +- src/modules/helpers/messageMedia.ts | 71 +++++++++++-------- src/modules/selectors/messages.ts | 3 +- 18 files changed, 166 insertions(+), 92 deletions(-) diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index f0ea2376c..f655d447f 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -27,7 +27,7 @@ import { DELETED_COMMENTS_CHANNEL_ID, LOCAL_MESSAGE_ID_BASE, SERVICE_NOTIFICATIO import { pick } from '../../../util/iteratees'; import { getApiChatIdFromMtpPeer } from './chats'; import { buildStickerFromDocument } from './symbols'; -import { buildApiPhoto, buildApiThumbnailFromStripped, buildApiPhotoSize } from './common'; +import { buildApiPhoto, buildApiThumbnailFromStripped } from './common'; import { interpolateArray } from '../../../util/waveform'; import { getCurrencySign } from '../../../components/middle/helpers/getCurrencySign'; import { buildPeer } from '../gramjsBuilders'; @@ -511,26 +511,25 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef const { id, photo, document } = media.webpage; + let video; + if (document instanceof GramJs.Document && document.mimeType.startsWith('video')) { + video = buildVideoFromDocument(document); + } + return { id: Number(id), ...pick(media.webpage, [ 'url', 'displayUrl', + 'type', 'siteName', 'title', 'description', + 'duration', ]), - photo: photo && photo instanceof GramJs.Photo - ? { - id: String(photo.id), - thumbnail: buildApiThumbnailFromStripped(photo.sizes), - sizes: photo.sizes - .filter((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize) - .map(buildApiPhotoSize), - } - : undefined, - // TODO support video and embed - ...(document && { hasDocument: true }), + photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined, + document: !video && document ? buildApiDocument(document) : undefined, + video, }; } diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 090a4dcc8..4a378896d 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -292,7 +292,12 @@ export function isMessageWithMedia(message: GramJs.Message | GramJs.UpdateServic || ( media instanceof GramJs.MessageMediaWebPage && media.webpage instanceof GramJs.WebPage - && media.webpage.photo instanceof GramJs.Photo + && ( + media.webpage.photo instanceof GramJs.Photo || ( + media.webpage.document instanceof GramJs.Document + && media.webpage.document.mimeType.startsWith('video') + ) + ) ) ); } diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers.ts index 63a167cba..8a7fca1b6 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers.ts @@ -22,6 +22,15 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ localDb.documents[String(message.media.document.id)] = message.media.document; } + if ( + message instanceof GramJs.Message + && message.media instanceof GramJs.MessageMediaWebPage + && message.media.webpage instanceof GramJs.WebPage + && message.media.webpage.document instanceof GramJs.Document + ) { + localDb.documents[String(message.media.webpage.document.id)] = message.media.webpage.document; + } + if (message instanceof GramJs.MessageService && 'photo' in message.action) { addPhotoToLocalDb(message.action.photo); } diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index 2f9cfe1d9..1b59c936c 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -145,6 +145,11 @@ async function download( if (entity.media instanceof GramJs.MessageMediaDocument && entity.media.document instanceof GramJs.Document) { fullSize = entity.media.document.size; } + if (entity.media instanceof GramJs.MessageMediaWebPage + && entity.media.webpage instanceof GramJs.WebPage + && entity.media.webpage.document instanceof GramJs.Document) { + fullSize = entity.media.webpage.document.size; + } } else if (entity instanceof GramJs.Photo) { mimeType = 'image/jpeg'; } else if (entityType === 'sticker' && sizeType) { @@ -187,6 +192,12 @@ function getMessageMediaMimeType(message: GramJs.Message, sizeType?: string) { return message.media.document!.mimeType; } + if (message.media instanceof GramJs.MessageMediaWebPage + && message.media.webpage instanceof GramJs.WebPage + && message.media.webpage.document instanceof GramJs.Document) { + return message.media.webpage.document.mimeType; + } + return undefined; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index d5a860906..7ad70fb74 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -152,11 +152,14 @@ export interface ApiWebPage { id: number; url: string; displayUrl: string; + type?: string; siteName?: string; title?: string; description?: string; photo?: ApiPhoto; - hasDocument?: true; + duration?: number; + document?: ApiDocument; + video?: ApiVideo; } export interface ApiMessageForwardInfo { diff --git a/src/components/common/WebLink.scss b/src/components/common/WebLink.scss index 44084f534..a562b0de1 100644 --- a/src/components/common/WebLink.scss +++ b/src/components/common/WebLink.scss @@ -9,7 +9,7 @@ margin-top: 1.5rem; } - &.without-photo::before { + &.without-media::before { content: attr(data-initial); width: 3rem; height: 3rem; @@ -86,7 +86,7 @@ padding: .25rem 3.75rem 0 0; .Media, - &.without-photo::before { + &.without-media::before { left: auto; right: 0; } diff --git a/src/components/common/WebLink.tsx b/src/components/common/WebLink.tsx index eb301646d..e7a8ceaee 100644 --- a/src/components/common/WebLink.tsx +++ b/src/components/common/WebLink.tsx @@ -57,13 +57,14 @@ const WebLink: FC = ({ message, senderTitle, onMessageClick }) => { title, description, photo, + video, } = linkData; const truncatedDescription = !senderTitle && trimText(description, MAX_TEXT_LENGTH); const className = buildClassName( 'WebLink scroll-item', - !photo && 'without-photo', + (!photo && !video) && 'without-media', ); return ( diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index c502b736d..ca9f8c4ad 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -37,6 +37,7 @@ import { getMessagePhoto, getMessageVideo, getMessageWebPagePhoto, + getMessageWebPageVideo, getPhotoFullDimensions, getVideoDimensions, IDimensions, @@ -107,12 +108,15 @@ const MediaViewer: FC = ({ const animationKey = useRef(null); const isOpen = Boolean(avatarOwner || messageId); const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined; + const webPageVideo = message ? getMessageWebPageVideo(message) : undefined; const photo = message ? getMessagePhoto(message) : undefined; const video = message ? getMessageVideo(message) : undefined; const isWebPagePhoto = Boolean(webPagePhoto); - const isPhoto = Boolean(photo || webPagePhoto); - const isVideo = Boolean(video); - const isGif = video ? video.isGif : undefined; + 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'; @@ -129,10 +133,10 @@ const MediaViewer: FC = ({ const [isFooterHidden, setIsFooterHidden] = useState(false); const messageIds = useMemo(() => { - return isWebPagePhoto && messageId + return (isWebPagePhoto || isWebPageVideo) && messageId ? [messageId] : getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia); - }, [isWebPagePhoto, messageId, chatMessages, collectionIds, isFromSharedMedia]); + }, [isWebPagePhoto, isWebPageVideo, messageId, chatMessages, collectionIds, isFromSharedMedia]); const selectedMediaMessageIndex = messageId ? messageIds.indexOf(messageId) : -1; const isFirst = selectedMediaMessageIndex === 0 || selectedMediaMessageIndex === -1; @@ -185,9 +189,11 @@ const MediaViewer: FC = ({ } const photoDimensions = isPhoto ? getPhotoFullDimensions(( - isWebPagePhoto ? getMessageWebPagePhoto(message!) : getMessagePhoto(message!) + isWebPagePhoto ? webPagePhoto : photo + )!) : undefined; + const videoDimensions = isVideo ? getVideoDimensions(( + isWebPageVideo ? webPageVideo : video )!) : undefined; - const videoDimensions = isVideo ? getVideoDimensions(getMessageVideo(message!)!) : undefined; useEffect(() => { if (!IS_SINGLE_COLUMN_LAYOUT) { @@ -436,7 +442,7 @@ const MediaViewer: FC = ({ posterData={bestImageData} posterSize={message && calculateMediaViewerDimensions(videoDimensions!, hasFooter, true)} downloadProgress={downloadProgress} - fileSize={video!.size} + fileSize={messageVideo!.size} isMediaViewerOpen={isOpen} noPlay={!isActive} onClose={close} diff --git a/src/components/mediaViewer/helpers/ghostAnimation.ts b/src/components/mediaViewer/helpers/ghostAnimation.ts index ec944891a..86bc72887 100644 --- a/src/components/mediaViewer/helpers/ghostAnimation.ts +++ b/src/components/mediaViewer/helpers/ghostAnimation.ts @@ -2,7 +2,13 @@ import { ApiMessage } from '../../../api/types'; import { MediaViewerOrigin } from '../../../types'; import { ANIMATION_END_DELAY } from '../../../config'; -import { getMessageContent, getPhotoFullDimensions, getVideoDimensions } from '../../../modules/helpers'; +import { + getMessageContent, + getMessageWebPagePhoto, + getMessageWebPageVideo, + getPhotoFullDimensions, + getVideoDimensions, +} from '../../../modules/helpers'; import { AVATAR_FULL_DIMENSIONS, calculateDimensions, @@ -28,9 +34,13 @@ export function animateOpening( let isVideo = false; let mediaSize; if (message) { - const { photo, video, webPage } = getMessageContent(message); - isVideo = Boolean(video); - mediaSize = video ? getVideoDimensions(video)! : getPhotoFullDimensions((photo || webPage!.photo)!)!; + 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; } diff --git a/src/components/middle/composer/WebPagePreview.scss b/src/components/middle/composer/WebPagePreview.scss index 2c5efc860..dea50d526 100644 --- a/src/components/middle/composer/WebPagePreview.scss +++ b/src/components/middle/composer/WebPagePreview.scss @@ -39,6 +39,10 @@ bottom: .05rem; } + &.with-video .media-inner { // TODO add support for video in previews in composer + display: none; + } + .site-title, .site-description { flex: 1; diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 7117978a8..31053a005 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -667,6 +667,8 @@ const Message: FC = ({ observeIntersection={observeIntersectionForMedia} noAvatars={noAvatars} shouldAutoLoad={shouldAutoLoadMedia} + shouldAutoPlay={shouldAutoPlayMedia} + lastSyncTime={lastSyncTime} onMediaClick={handleMediaClick} onCancelMediaTransfer={handleCancelUpload} /> diff --git a/src/components/middle/message/Video.tsx b/src/components/middle/message/Video.tsx index 9ac8801d3..ef1e586ec 100644 --- a/src/components/middle/message/Video.tsx +++ b/src/components/middle/message/Video.tsx @@ -9,10 +9,11 @@ import { formatMediaDuration } from '../../../util/dateFormat'; import buildClassName from '../../../util/buildClassName'; import { calculateVideoDimensions } from '../../common/helpers/mediaDimensions'; import { - canMessagePlayVideoInline, getMediaTransferState, getMessageMediaFormat, getMessageMediaHash, + getMessageVideo, + getMessageWebPageVideo, isForwardedMessage, isOwnMessage, } from '../../../modules/helpers'; @@ -62,9 +63,8 @@ const Video: FC = ({ // eslint-disable-next-line no-null/no-null const videoRef = useRef(null); - const video = message.content.video!; + const video = (getMessageVideo(message) || getMessageWebPageVideo(message))!; const localBlobUrl = video.blobUrl; - const canPlayInline = Boolean(localBlobUrl) || canMessagePlayVideoInline(video); const isIntersecting = useIsIntersecting(ref, observeIntersection); @@ -87,13 +87,13 @@ const Video: FC = ({ ); const fullMediaData = localBlobUrl || mediaData; - const isInline = Boolean(canPlayInline && isIntersecting && fullMediaData); + const isInline = Boolean(isIntersecting && fullMediaData); const { isBuffered, bufferingHandlers } = useBuffering(!shouldAutoLoad); const { isUploading, isTransferring, transferProgress } = getMediaTransferState( message, uploadProgress || downloadProgress, - shouldDownload && (canPlayInline && !isBuffered), + shouldDownload && !isBuffered, ); const wasDownloadDisabled = usePrevious(isDownloadAllowed) === false; const { @@ -107,6 +107,8 @@ const Video: FC = ({ setPlayProgress(Math.max(0, e.currentTarget.currentTime - 1)); }, []); + const duration = video.duration || (videoRef.current && videoRef.current.duration) || 0; + const isOwn = isOwnMessage(message); const isForwarded = isForwardedMessage(message); const { width, height } = dimensions || calculateVideoDimensions(video, isOwn, isForwarded, noAvatars); @@ -122,15 +124,15 @@ const Video: FC = ({ if (onCancelUpload) { onCancelUpload(message); } - } else if (canPlayInline && !fullMediaData) { + } else if (!fullMediaData) { setIsDownloadAllowed((isAllowed) => !isAllowed); - } else if (canPlayInline && fullMediaData && !isPlayAllowed) { + } else if (fullMediaData && !isPlayAllowed) { setIsPlayAllowed(true); videoRef.current!.play(); } else if (onClick) { onClick(message.id); } - }, [isUploading, canPlayInline, fullMediaData, isPlayAllowed, onClick, onCancelUpload, message]); + }, [isUploading, fullMediaData, isPlayAllowed, onClick, onCancelUpload, message]); const className = buildClassName('media-inner dark', !isUploading && 'interactive'); const videoClassName = buildClassName('full-media', transitionClassNames); @@ -140,9 +142,8 @@ const Video: FC = ({ : ''; const shouldRenderInlineVideo = isInline; - const shouldRenderHqPreview = !canPlayInline && mediaData; - const shouldRenderPlayButton = !canPlayInline || (isDownloadAllowed && !isPlayAllowed && !shouldRenderSpinner); - const shouldRenderDownloadButton = canPlayInline && !isDownloadAllowed; + const shouldRenderPlayButton = (isDownloadAllowed && !isPlayAllowed && !shouldRenderSpinner); + const shouldRenderDownloadButton = !isDownloadAllowed; return (
= ({ )} - {shouldRenderHqPreview && ( - - )} {shouldRenderPlayButton && ( )} @@ -209,13 +201,11 @@ const Video: FC = ({ {shouldRenderDownloadButton && ( )} - {isTransferring && !canPlayInline ? ( - {Math.round(transferProgress * 100)}% - ) : isTransferring && canPlayInline ? ( + {isTransferring ? ( ... ) : (
- {video.isGif ? 'GIF' : formatMediaDuration(video.duration - playProgress)} + {video.isGif ? 'GIF' : formatMediaDuration(Math.max(duration - playProgress, 0))}
)}
diff --git a/src/components/middle/message/WebPage.scss b/src/components/middle/message/WebPage.scss index 1fb4fea05..fcc8f091a 100644 --- a/src/components/middle/message/WebPage.scss +++ b/src/components/middle/message/WebPage.scss @@ -46,6 +46,11 @@ } } + &.with-video .media-inner{ + margin-top: 0.5rem !important; + margin-bottom: 1rem !important; + } + &.with-square-photo { display: flex; margin-bottom: 1rem; diff --git a/src/components/middle/message/WebPage.tsx b/src/components/middle/message/WebPage.tsx index 80f4f6b88..2b5862c3e 100644 --- a/src/components/middle/message/WebPage.tsx +++ b/src/components/middle/message/WebPage.tsx @@ -11,6 +11,7 @@ import buildClassName from '../../../util/buildClassName'; import SafeLink from '../../common/SafeLink'; import Photo from './Photo'; +import Video from './Video'; import './WebPage.scss'; @@ -21,7 +22,9 @@ type OwnProps = { observeIntersection?: ObserveFn; noAvatars?: boolean; shouldAutoLoad?: boolean; + shouldAutoPlay?: boolean; inPreview?: boolean; + lastSyncTime?: number; onMediaClick?: () => void; onCancelMediaTransfer?: () => void; }; @@ -31,7 +34,9 @@ const WebPage: FC = ({ observeIntersection, noAvatars, shouldAutoLoad, + shouldAutoPlay, inPreview, + lastSyncTime, onMediaClick, onCancelMediaTransfer, }) => { @@ -58,16 +63,16 @@ const WebPage: FC = ({ title, description, photo, + video, } = webPage; - - const isMediaInteractive = photo && onMediaClick && !isSquarePhoto && !webPage.hasDocument; + const isMediaInteractive = (photo || video) && onMediaClick && !isSquarePhoto; const truncatedDescription = trimText(description, MAX_TEXT_LENGTH); const className = buildClassName( 'WebPage', - photo - ? (isSquarePhoto && 'with-square-photo') - : (!inPreview && 'without-photo'), + isSquarePhoto && 'with-square-photo', + !photo && !video && !inPreview && 'without-media', + video && 'with-video', ); return ( @@ -76,7 +81,7 @@ const WebPage: FC = ({ data-initial={(siteName || displayUrl)[0]} dir="auto" > - {photo && ( + {photo && !video && ( = ({

{renderText(truncatedDescription, ['emoji', 'br'])}

)} + {!inPreview && video && ( +