diff --git a/src/api/gramjs/apiBuilders/common.ts b/src/api/gramjs/apiBuilders/common.ts index ebc10bfed..f755e3abe 100644 --- a/src/api/gramjs/apiBuilders/common.ts +++ b/src/api/gramjs/apiBuilders/common.ts @@ -50,7 +50,7 @@ export function buildApiThumbnailFromCached(photoSize: GramJs.PhotoCachedSize): export function buildApiThumbnailFromPath( photoSize: GramJs.PhotoPathSize, - sizeAttribute: GramJs.DocumentAttributeImageSize, + sizeAttribute: GramJs.DocumentAttributeImageSize | GramJs.DocumentAttributeVideo, ): ApiThumbnail | undefined { const { w, h } = sizeAttribute; const dataUri = `data:image/svg+xml;utf8,${pathBytesToSvg(photoSize.bytes, w, h)}`; diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index efad15291..1bb7faea9 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -155,7 +155,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM const forwardInfo = mtpMessage.fwdFrom && buildApiMessageForwardInfo(mtpMessage.fwdFrom, isChatWithSelf); const { replies, mediaUnread: isMediaUnread, postAuthor } = mtpMessage; const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId); - const isInAlbum = Boolean(groupedId) && !(content.document || content.audio); + const isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker); const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide; return { @@ -192,13 +192,13 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiReactions { const { - recentReactons, results, canSeeList, + recentReactions, results, canSeeList, } = reactions; return { canSeeList, results: results.map(buildReactionCount), - recentReactions: recentReactons?.map(buildMessageUserReaction), + recentReactions: recentReactions?.map(buildMessagePeerReaction), }; } @@ -212,11 +212,11 @@ function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCou }; } -export function buildMessageUserReaction(userReaction: GramJs.MessageUserReaction): ApiUserReaction { - const { userId, reaction } = userReaction; +export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiUserReaction { + const { peerId, reaction } = userReaction; return { - userId: buildApiPeerId(userId, 'user'), + userId: getApiChatIdFromMtpPeer(peerId), reaction, }; } diff --git a/src/api/gramjs/apiBuilders/symbols.ts b/src/api/gramjs/apiBuilders/symbols.ts index ed07574ff..5eb2d7086 100644 --- a/src/api/gramjs/apiBuilders/symbols.ts +++ b/src/api/gramjs/apiBuilders/symbols.ts @@ -2,12 +2,13 @@ import { Api as GramJs } from '../../../lib/gramjs'; import { ApiEmojiInteraction, ApiSticker, ApiStickerSet, GramJsEmojiInteraction, } from '../../types'; -import { MEMOJI_STICKER_ID } from '../../../config'; +import { NO_STICKER_SET_ID } from '../../../config'; import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common'; import localDb from '../localDb'; -const ANIMATED_STICKER_MIME_TYPE = 'application/x-tgsticker'; +const LOTTIE_STICKER_MIME_TYPE = 'application/x-tgsticker'; +const GIF_STICKER_MIME_TYPE = 'video/webm'; export function buildStickerFromDocument(document: GramJs.TypeDocument): ApiSticker | undefined { if (document instanceof GramJs.DocumentEmpty) { @@ -19,31 +20,55 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument): ApiStic attr instanceof GramJs.DocumentAttributeSticker )); - const fileAttribute = document.mimeType === ANIMATED_STICKER_MIME_TYPE && document.attributes - .find((attr: any): attr is GramJs.DocumentAttributeFilename => ( - attr instanceof GramJs.DocumentAttributeFilename - )); + const fileAttribute = (document.mimeType === LOTTIE_STICKER_MIME_TYPE || document.mimeType === GIF_STICKER_MIME_TYPE) + && document.attributes + .find((attr: any): attr is GramJs.DocumentAttributeFilename => ( + attr instanceof GramJs.DocumentAttributeFilename + )); if (!stickerAttribute && !fileAttribute) { return undefined; } - const sizeAttribute = document.attributes + const isLottie = document.mimeType === LOTTIE_STICKER_MIME_TYPE; + const isGif = document.mimeType === GIF_STICKER_MIME_TYPE; + + const imageSizeAttribute = document.attributes .find((attr: any): attr is GramJs.DocumentAttributeImageSize => ( attr instanceof GramJs.DocumentAttributeImageSize )); + const videoSizeAttribute = document.attributes + .find((attr: any): attr is GramJs.DocumentAttributeVideo => ( + attr instanceof GramJs.DocumentAttributeVideo + )); + + const sizeAttribute = imageSizeAttribute || videoSizeAttribute; + const stickerSetInfo = stickerAttribute && stickerAttribute.stickerset instanceof GramJs.InputStickerSetID ? stickerAttribute.stickerset : undefined; const emoji = stickerAttribute?.alt; - const isAnimated = document.mimeType === ANIMATED_STICKER_MIME_TYPE; + const cachedThumb = document.thumbs && document.thumbs.find( (s): s is GramJs.PhotoCachedSize => s instanceof GramJs.PhotoCachedSize, ); + + // eslint-disable-next-line no-restricted-globals + if (document.mimeType === GIF_STICKER_MIME_TYPE && !(self as any).isWebmSupported && !cachedThumb) { + const staticThumb = document.thumbs && document.thumbs.find( + (s): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize, + ); + + if (!staticThumb) { + return undefined; + } + } + const pathThumb = document.thumbs && document.thumbs.find( (s): s is GramJs.PhotoPathSize => s instanceof GramJs.PhotoPathSize, ); + const thumbnail = cachedThumb ? ( buildApiThumbnailFromCached(cachedThumb) ) : pathThumb && sizeAttribute ? ( @@ -54,10 +79,11 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument): ApiStic return { id: String(document.id), - stickerSetId: stickerSetInfo ? String(stickerSetInfo.id) : MEMOJI_STICKER_ID, + stickerSetId: stickerSetInfo ? String(stickerSetInfo.id) : NO_STICKER_SET_ID, stickerSetAccessHash: stickerSetInfo && String(stickerSetInfo.accessHash), emoji, - isAnimated, + isLottie, + isGif, width, height, thumbnail, @@ -69,6 +95,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet { archived, animated, installedDate, + gifs, id, accessHash, title, @@ -79,7 +106,8 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet { return { archived, - isAnimated: animated, + isLottie: animated, + isGifs: gifs, installedDate, id: String(id), accessHash: String(accessHash), diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 23f2d2045..6ce6cbd4f 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -47,12 +47,14 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) onUpdate = _onUpdate; const { - userAgent, platform, sessionData, isTest, isMovSupported, + userAgent, platform, sessionData, isTest, isMovSupported, isWebmSupported, } = initialArgs; const session = new sessions.CallbackSession(sessionData, onSessionUpdate); // eslint-disable-next-line no-restricted-globals (self as any).isMovSupported = isMovSupported; + // eslint-disable-next-line no-restricted-globals + (self as any).isWebmSupported = isWebmSupported; client = new TelegramClient( session, diff --git a/src/api/gramjs/methods/reactions.ts b/src/api/gramjs/methods/reactions.ts index 1a086a1ba..a274fcd6d 100644 --- a/src/api/gramjs/methods/reactions.ts +++ b/src/api/gramjs/methods/reactions.ts @@ -3,7 +3,7 @@ import { invokeRequest } from './client'; import { Api as GramJs } from '../../../lib/gramjs'; import { buildInputPeer } from '../gramjsBuilders'; import localDb from '../localDb'; -import { buildApiAvailableReaction, buildMessageUserReaction } from '../apiBuilders/messages'; +import { buildApiAvailableReaction, buildMessagePeerReaction } from '../apiBuilders/messages'; import { REACTION_LIST_LIMIT } from '../../../config'; import { addEntitiesWithPhotosToLocalDb } from '../helpers'; import { buildApiUser } from '../apiBuilders/users'; @@ -120,7 +120,7 @@ export async function fetchMessageReactionsList({ return { users: result.users.map(buildApiUser).filter(Boolean as any), nextOffset, - reactions: reactions.map(buildMessageUserReaction), + reactions: reactions.map(buildMessagePeerReaction), count, }; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 9023cacc0..283875b7b 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -25,7 +25,8 @@ export interface ApiSticker { stickerSetId: string; stickerSetAccessHash?: string; emoji?: string; - isAnimated: boolean; + isLottie: boolean; + isGif: boolean; width?: number; height?: number; thumbnail?: ApiThumbnail; @@ -34,7 +35,8 @@ export interface ApiSticker { export interface ApiStickerSet { archived?: true; - isAnimated?: true; + isLottie?: true; + isGifs?: true; installedDate?: number; id: string; accessHash: string; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 9d5f7ef60..b7bb43366 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -6,6 +6,7 @@ export interface ApiInitialArgs { sessionData?: ApiSessionData; isTest?: boolean; isMovSupported?: boolean; + isWebmSupported?: boolean; } export interface ApiOnProgress { diff --git a/src/components/common/StickerButton.scss b/src/components/common/StickerButton.scss index 427103036..ee4e2a9dd 100644 --- a/src/components/common/StickerButton.scss +++ b/src/components/common/StickerButton.scss @@ -38,7 +38,7 @@ margin: 0; } - .AnimatedSticker, img { + .AnimatedSticker, img, video { position: absolute; top: 0; left: 0; @@ -46,7 +46,7 @@ height: 100%; } - img { + img, video { object-fit: contain; } diff --git a/src/components/common/StickerButton.tsx b/src/components/common/StickerButton.tsx index 6226de9d5..adba636f6 100644 --- a/src/components/common/StickerButton.tsx +++ b/src/components/common/StickerButton.tsx @@ -11,6 +11,8 @@ import useShowTransition from '../../hooks/useShowTransition'; import useFlag from '../../hooks/useFlag'; import buildClassName from '../../util/buildClassName'; import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur'; +import safePlay from '../../util/safePlay'; +import { IS_WEBM_SUPPORTED } from '../../util/environment'; import AnimatedSticker from './AnimatedSticker'; import Button from '../ui/Button'; @@ -44,12 +46,15 @@ const StickerButton: FC = ({ const previewBlobUrl = useMedia(`${localMediaHash}?size=m`, !isIntersecting, ApiMediaFormat.BlobUrl); const shouldPlay = isIntersecting && !noAnimate; - const lottieData = useMedia(sticker.isAnimated && localMediaHash, !shouldPlay, ApiMediaFormat.Lottie); - const [isAnimationLoaded, markLoaded, unmarkLoaded] = useFlag(Boolean(lottieData)); - const canAnimatedPlay = isAnimationLoaded && shouldPlay; + const lottieData = useMedia(sticker.isLottie && localMediaHash, !shouldPlay, ApiMediaFormat.Lottie); + const [isLottieLoaded, markLoaded, unmarkLoaded] = useFlag(Boolean(lottieData)); + const canLottiePlay = isLottieLoaded && shouldPlay; + const isGif = sticker.isGif && IS_WEBM_SUPPORTED; + const gifBlobUrl = useMedia(isGif && localMediaHash, !shouldPlay, ApiMediaFormat.BlobUrl); + const canGifPlay = Boolean(isGif && gifBlobUrl && shouldPlay); const { transitionClassNames: previewTransitionClassNames } = useShowTransition( - Boolean(previewBlobUrl || canAnimatedPlay), + Boolean(previewBlobUrl || canLottiePlay), undefined, undefined, 'slow', @@ -62,6 +67,17 @@ const StickerButton: FC = ({ } }, [unmarkLoaded, shouldPlay]); + useEffect(() => { + if (!isGif || !ref.current) return; + const video = ref.current.querySelector('video'); + if (!video) return; + if (canGifPlay) { + safePlay(video); + } else { + video.pause(); + } + }, [isGif, canGifPlay]); + function handleClick() { if (onClick) { onClick(clickArg); @@ -78,12 +94,11 @@ const StickerButton: FC = ({ const fullClassName = buildClassName( 'StickerButton', onClick && 'interactive', - sticker.isAnimated && 'animated', stickerSelector, className, ); - const style = thumbDataUri && !canAnimatedPlay ? `background-image: url('${thumbDataUri}');` : ''; + const style = (thumbDataUri && !canLottiePlay && !canGifPlay) ? `background-image: url('${thumbDataUri}');` : ''; return (
= ({ onMouseDown={preventMessageInputBlurWithBubbling} onClick={handleClick} > - {!canAnimatedPlay && ( + {!canLottiePlay && !canGifPlay && ( // eslint-disable-next-line jsx-a11y/alt-text )} + {isGif && ( +