Message: Support web page videos; Always play videos inline (#1233)

This commit is contained in:
Alexander Zinchuk 2021-07-06 19:12:42 +03:00
parent f15d616e20
commit 5facc1e705
18 changed files with 166 additions and 92 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,13 +57,14 @@ const WebLink: FC<OwnProps> = ({ 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 (

View File

@ -37,6 +37,7 @@ import {
getMessagePhoto,
getMessageVideo,
getMessageWebPagePhoto,
getMessageWebPageVideo,
getPhotoFullDimensions,
getVideoDimensions,
IDimensions,
@ -107,12 +108,15 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
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 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<StateProps & DispatchProps> = ({
const [isFooterHidden, setIsFooterHidden] = useState<boolean>(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<StateProps & DispatchProps> = ({
}
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<StateProps & DispatchProps> = ({
posterData={bestImageData}
posterSize={message && calculateMediaViewerDimensions(videoDimensions!, hasFooter, true)}
downloadProgress={downloadProgress}
fileSize={video!.size}
fileSize={messageVideo!.size}
isMediaViewerOpen={isOpen}
noPlay={!isActive}
onClose={close}

View File

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

View File

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

View File

@ -667,6 +667,8 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
observeIntersection={observeIntersectionForMedia}
noAvatars={noAvatars}
shouldAutoLoad={shouldAutoLoadMedia}
shouldAutoPlay={shouldAutoPlayMedia}
lastSyncTime={lastSyncTime}
onMediaClick={handleMediaClick}
onCancelMediaTransfer={handleCancelUpload}
/>

View File

@ -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<OwnProps> = ({
// eslint-disable-next-line no-null/no-null
const videoRef = useRef<HTMLVideoElement>(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<OwnProps> = ({
);
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<OwnProps> = ({
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<OwnProps> = ({
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<OwnProps> = ({
: '';
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 (
<div
@ -189,15 +190,6 @@ const Video: FC<OwnProps> = ({
<source src={fullMediaData} />
</video>
)}
{shouldRenderHqPreview && (
<img
src={mediaData}
className={`full-media ${transitionClassNames}`}
width={width}
height={height}
alt=""
/>
)}
{shouldRenderPlayButton && (
<i className="icon-large-play" />
)}
@ -209,13 +201,11 @@ const Video: FC<OwnProps> = ({
{shouldRenderDownloadButton && (
<i className="icon-download" />
)}
{isTransferring && !canPlayInline ? (
<span className="message-upload-progress">{Math.round(transferProgress * 100)}%</span>
) : isTransferring && canPlayInline ? (
{isTransferring ? (
<span className="message-upload-progress">...</span>
) : (
<div className="message-media-duration">
{video.isGif ? 'GIF' : formatMediaDuration(video.duration - playProgress)}
{video.isGif ? 'GIF' : formatMediaDuration(Math.max(duration - playProgress, 0))}
</div>
)}
</div>

View File

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

View File

@ -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<OwnProps> = ({
observeIntersection,
noAvatars,
shouldAutoLoad,
shouldAutoPlay,
inPreview,
lastSyncTime,
onMediaClick,
onCancelMediaTransfer,
}) => {
@ -58,16 +63,16 @@ const WebPage: FC<OwnProps> = ({
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<OwnProps> = ({
data-initial={(siteName || displayUrl)[0]}
dir="auto"
>
{photo && (
{photo && !video && (
<Photo
message={message}
observeIntersection={observeIntersection}
@ -97,6 +102,18 @@ const WebPage: FC<OwnProps> = ({
<p className="site-description">{renderText(truncatedDescription, ['emoji', 'br'])}</p>
)}
</div>
{!inPreview && video && (
<Video
message={message}
observeIntersection={observeIntersection!}
noAvatars={noAvatars}
shouldAutoLoad={shouldAutoLoad}
shouldAutoPlay={shouldAutoPlay}
lastSyncTime={lastSyncTime}
onClick={isMediaInteractive ? handleMediaClick : undefined}
onCancelUpload={onCancelMediaTransfer}
/>
)}
</div>
);
};

View File

@ -313,7 +313,7 @@
}
}
.message-content.media {
.message-content.media, .WebPage {
.media-inner {
display: flex;
justify-content: center;

View File

@ -65,7 +65,7 @@ export function buildContentClassName(
} else if (webPage) {
classNames.push('web-page');
if (webPage.photo) {
if (webPage.photo || webPage.video) {
classNames.push('media');
}
}

View File

@ -13,7 +13,6 @@ export type IDimensions = {
type Target = 'micro' | 'pictogram' | 'inline' | 'viewerPreview' | 'viewerFull' | 'download';
const MAX_INLINE_VIDEO_SIZE = 10 * 1024 ** 2; // 10 MB
export function getMessageContent(message: ApiMessage) {
return message.content;
@ -88,12 +87,24 @@ export function getMessageWebPagePhoto(message: ApiMessage) {
return webPage ? webPage.photo : undefined;
}
export function getMessageWebPageDocument(message: ApiMessage) {
const webPage = getMessageWebPage(message);
return webPage ? webPage.document : undefined;
}
export function getMessageWebPageVideo(message: ApiMessage): ApiVideo | undefined {
const webPage = getMessageWebPage(message);
if (!webPage) return undefined;
return webPage.video;
}
export function getMessageMediaThumbnail(message: ApiMessage) {
const media = getMessagePhoto(message)
|| getMessageVideo(message)
|| getMessageDocument(message)
|| getMessageSticker(message)
|| getMessageWebPagePhoto(message);
|| getMessageWebPagePhoto(message)
|| getMessageWebPageVideo(message);
if (!media) {
return undefined;
@ -116,32 +127,34 @@ export function getMessageMediaHash(
photo, video, sticker, audio, voice, document,
} = message.content;
const webPagePhoto = getMessageWebPagePhoto(message);
const webPageVideo = getMessageWebPageVideo(message);
if (!(photo || video || sticker || webPagePhoto || audio || voice || document)) {
const messageVideo = video || webPageVideo;
const messagePhoto = photo || webPagePhoto;
if (!(messagePhoto || messageVideo || sticker || audio || voice || document)) {
return undefined;
}
const base = getMessageKey(message);
if (photo || webPagePhoto) {
if (messageVideo) {
switch (target) {
case 'micro':
case 'pictogram':
return `${base}?size=m`;
case 'inline':
if (hasMessageLocalBlobUrl(message)) {
return undefined;
}
return `${base}?size=x`;
return !hasMessageLocalBlobUrl(message) ? getVideoOrAudioBaseHash(messageVideo, base) : undefined;
case 'viewerPreview':
return `${base}?size=x`;
return `${base}?size=m`;
case 'viewerFull':
return `${base}?size=z`;
return getVideoOrAudioBaseHash(messageVideo, base);
case 'download':
return `${base}?download`;
}
}
if (video) {
if (messagePhoto) {
switch (target) {
case 'micro':
case 'pictogram':
@ -151,17 +164,11 @@ export function getMessageMediaHash(
return undefined;
}
if (canMessagePlayVideoInline(video)) {
return getVideoOrAudioBaseHash(video, base);
}
return `${base}?size=z`;
return `${base}?size=x`;
case 'viewerPreview':
return `${base}?size=m`;
return `${base}?size=x`;
case 'viewerFull':
return getVideoOrAudioBaseHash(video, base);
case 'download':
return `${base}?download`;
return `${base}?size=z`;
}
}
@ -235,10 +242,12 @@ export function getMessageMediaFormat(
sticker, video, audio, voice,
} = message.content;
const fullVideo = video || getMessageWebPageVideo(message);
if (sticker && target === 'inline' && sticker.isAnimated) {
return ApiMediaFormat.Lottie;
} else if (video && IS_PROGRESSIVE_SUPPORTED && (
(target === 'viewerFull') || (target === 'inline' && canMessagePlayVideoInline(video))
} else if (fullVideo && IS_PROGRESSIVE_SUPPORTED && (
target === 'viewerFull' || target === 'inline'
)) {
return ApiMediaFormat.Progressive;
} else if (audio || voice) {
@ -254,12 +263,18 @@ export function getMessageMediaFormat(
}
export function getMessageMediaFilename(message: ApiMessage) {
const { photo, video, webPage } = message.content;
const { photo, video } = message.content;
const webPagePhoto = getMessageWebPagePhoto(message);
const webPageVideo = getMessageWebPageVideo(message);
if (photo || (webPage && webPage.photo)) {
if (photo || webPagePhoto) {
return `photo${message.date}.jpeg`;
}
if (webPageVideo) {
return webPageVideo.fileName;
}
if (video) {
return video.fileName;
}
@ -273,10 +288,6 @@ export function hasMessageLocalBlobUrl(message: ApiMessage) {
return (photo && photo.blobUrl) || (video && video.blobUrl) || (document && document.previewBlobUrl);
}
export function canMessagePlayVideoInline(video: ApiVideo): boolean {
return video.isGif || video.isRound || video.size <= MAX_INLINE_VIDEO_SIZE;
}
export function getChatMediaMessageIds(
messages: Record<number, ApiMessage>, listedIds: number[], reverseOrder = false,
) {
@ -360,7 +371,7 @@ export function getMessageContentIds(
export function getMediaDuration(message: ApiMessage) {
const { audio, voice, video } = getMessageContent(message);
const media = audio || voice || video;
const media = audio || voice || video || getMessageWebPageVideo(message);
if (!media) {
return undefined;
}

View File

@ -29,6 +29,7 @@ import {
isChatGroup,
isChatSuperGroup,
getMessageVideo,
getMessageWebPageVideo,
} from '../helpers';
import { findLast } from '../../util/iteratees';
import { selectIsStickerFavorite } from './symbols';
@ -679,7 +680,7 @@ export function selectShouldAutoLoadMedia(
}
export function selectShouldAutoPlayMedia(global: GlobalState, message: ApiMessage) {
const video = getMessageVideo(message);
const video = getMessageVideo(message) || getMessageWebPageVideo(message);
if (!video) {
return undefined;
}