Web Page: Display stickerset preview (#4529)

This commit is contained in:
Alexander Zinchuk 2024-05-03 14:38:26 +02:00
parent 1f834d42ed
commit 85bc6cb297
8 changed files with 136 additions and 36 deletions

View File

@ -19,6 +19,7 @@ import type {
ApiVoice,
ApiWebDocument,
ApiWebPage,
ApiWebPageStickerData,
ApiWebPageStoryData,
MediaContent,
} from '../../types';
@ -35,7 +36,7 @@ import {
buildApiThumbnailFromStripped,
} from './common';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildStickerFromDocument } from './symbols';
import { buildStickerFromDocument, processStickerResult } from './symbols';
export function buildMessageContent(
mtpMessage: UniversalMessage | GramJs.UpdateServiceNotification,
@ -694,8 +695,9 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
audio = buildAudioFromDocument(document);
}
let story: ApiWebPageStoryData | undefined;
let stickers: ApiWebPageStickerData | undefined;
const attributeStory = attributes
?.find((a: any): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory);
?.find((a): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory);
if (attributeStory) {
const peerId = getApiChatIdFromMtpPeer(attributeStory.peer);
story = {
@ -707,6 +709,16 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
addStoryToLocalDb(attributeStory.story, peerId);
}
}
const attributeStickers = attributes?.find((a): a is GramJs.WebPageAttributeStickerSet => (
a instanceof GramJs.WebPageAttributeStickerSet
));
if (attributeStickers) {
stickers = {
documents: processStickerResult(attributeStickers.stickers),
isEmoji: attributeStickers.emojis,
isWithTextColor: attributeStickers.textColor,
};
}
return {
id: Number(id),
@ -724,6 +736,7 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
video,
audio,
story,
stickers,
};
}

View File

@ -3,7 +3,7 @@ import type { ApiWebDocument } from './bots';
import type { ApiGroupCall, PhoneCallAction } from './calls';
import type { ApiChat, ApiPeerColor } from './chats';
import type { ApiInputStorePaymentPurpose, ApiPremiumGiftCodeOption } from './payments';
import type { ApiMessageStoryData, ApiWebPageStoryData } from './stories';
import type { ApiMessageStoryData, ApiWebPageStickerData, ApiWebPageStoryData } from './stories';
export interface ApiDimensions {
width: number;
@ -380,6 +380,7 @@ export interface ApiWebPage {
document?: ApiDocument;
video?: ApiVideo;
story?: ApiWebPageStoryData;
stickers?: ApiWebPageStickerData;
}
export type ApiReplyInfo = ApiMessageReplyInfo | ApiStoryReplyInfo;

View File

@ -1,6 +1,6 @@
import type { ApiPrivacySettings } from '../../types';
import type {
ApiGeoPoint, ApiMessage, ApiReaction, ApiReactionCount, ApiStoryForwardInfo, MediaContent,
ApiGeoPoint, ApiMessage, ApiReaction, ApiReactionCount, ApiSticker, ApiStoryForwardInfo, MediaContent,
} from './messages';
export interface ApiStory {
@ -73,6 +73,12 @@ export type ApiWebPageStoryData = {
peerId: string;
};
export type ApiWebPageStickerData = {
documents: ApiSticker[];
isEmoji?: boolean;
isWithTextColor?: boolean;
};
export type ApiStoryViewPublicForward = {
type: 'forward';
peerId: string;

View File

@ -1230,7 +1230,8 @@ const Message: FC<OwnProps & StateProps> = ({
{webPage && (
<WebPage
message={message}
observeIntersection={observeIntersectionForLoading}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
noAvatars={noAvatars}
canAutoLoad={canAutoLoadMedia}
canAutoPlay={canAutoPlayMedia}

View File

@ -13,7 +13,7 @@
&.with-video {
padding: 0.1875rem 0.375rem;
}
&.with-document {
--file-icon-border-color: var(--accent-background-color);
}
@ -22,6 +22,23 @@
margin: -0.375rem;
}
&--emoji-grid {
display: grid !important;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 0.25rem;
}
&--sticker {
position: relative;
width: 100%;
height: 100%;
}
&--stickers {
color: var(--accent-color);
}
&.in-preview {
border-radius: 0.25rem;
background-color: var(--color-primary-tint);

View File

@ -1,5 +1,5 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact';
import React, { memo, useRef } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { ApiMessage, ApiTypeStory } from '../../../api/types';
@ -13,6 +13,7 @@ import renderText from '../../common/helpers/renderText';
import { calculateMediaDimensions } from './helpers/mediaDimensions';
import { getWebpageButtonText } from './helpers/webpageType';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useAppLayout from '../../../hooks/useAppLayout';
import useEnsureStory from '../../../hooks/useEnsureStory';
import useLang from '../../../hooks/useLang';
@ -22,6 +23,7 @@ import Audio from '../../common/Audio';
import Document from '../../common/Document';
import EmojiIconBackground from '../../common/embedded/EmojiIconBackground';
import SafeLink from '../../common/SafeLink';
import StickerView from '../../common/StickerView';
import Button from '../../ui/Button';
import BaseStory from './BaseStory';
import Photo from './Photo';
@ -31,10 +33,13 @@ import './WebPage.scss';
const MAX_TEXT_LENGTH = 170; // symbols
const WEBPAGE_STORY_TYPE = 'telegram_story';
const STICKER_SIZE = 80;
const EMOJI_SIZE = 40;
type OwnProps = {
message: ApiMessage;
observeIntersection?: ObserveFn;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
noAvatars?: boolean;
canAutoLoad?: boolean;
canAutoPlay?: boolean;
@ -55,7 +60,8 @@ type OwnProps = {
const WebPage: FC<OwnProps> = ({
message,
observeIntersection,
observeIntersectionForLoading,
observeIntersectionForPlaying,
noAvatars,
canAutoLoad,
canAutoPlay,
@ -76,6 +82,10 @@ const WebPage: FC<OwnProps> = ({
const { openTelegramLink } = getActions();
const webPage = getMessageWebPage(message);
const { isMobile } = useAppLayout();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const stickersRef = useRef<HTMLDivElement>(null);
const lang = useLang();
@ -90,10 +100,13 @@ const WebPage: FC<OwnProps> = ({
});
});
const { story: storyData } = webPage || {};
const { story: storyData, stickers } = webPage || {};
useEnsureStory(storyData?.peerId, storyData?.id, story);
const hasCustomColor = stickers?.isWithTextColor || stickers?.documents?.[0]?.shouldUseTextColor;
const customColor = useDynamicColorListener(stickersRef, !hasCustomColor);
if (!webPage) {
return undefined;
}
@ -115,7 +128,7 @@ const WebPage: FC<OwnProps> = ({
const quickButtonLangKey = !inPreview && !isExpiredStory ? getWebpageButtonText(type) : undefined;
const truncatedDescription = trimText(description, MAX_TEXT_LENGTH);
const isArticle = Boolean(truncatedDescription || title || siteName);
let isSquarePhoto = false;
let isSquarePhoto = Boolean(stickers);
if (isArticle && webPage?.photo && !webPage.video) {
const { width, height } = calculateMediaDimensions(message, undefined, undefined, isMobile);
isSquarePhoto = width === height;
@ -149,18 +162,25 @@ const WebPage: FC<OwnProps> = ({
return (
<div
ref={ref}
className={className}
data-initial={(siteName || displayUrl)[0]}
dir={lang.isRtl ? 'rtl' : 'auto'}
>
<div className={buildClassName('WebPage--content', isStory && 'is-story')}>
{backgroundEmojiId && (
<EmojiIconBackground
emojiDocumentId={backgroundEmojiId}
className="WebPage--background-icons"
/>
)}
{isStory && (
<BaseStory story={story} isProtected={isProtected} isConnected={isConnected} isPreview />
)}
{photo && !video && (
<Photo
message={message}
observeIntersection={observeIntersection}
observeIntersection={observeIntersectionForLoading}
noAvatars={noAvatars}
canAutoLoad={canAutoLoad}
size={isSquarePhoto ? 'pictogram' : 'inline'}
@ -175,12 +195,6 @@ const WebPage: FC<OwnProps> = ({
)}
{isArticle && (
<div className="WebPage-text">
{backgroundEmojiId && (
<EmojiIconBackground
emojiDocumentId={backgroundEmojiId}
className="WebPage--background-icons"
/>
)}
<SafeLink className="site-name" url={url} text={siteName || displayUrl} />
{!inPreview && title && (
<p className="site-title">{renderText(title)}</p>
@ -193,7 +207,7 @@ const WebPage: FC<OwnProps> = ({
{!inPreview && video && (
<Video
message={message}
observeIntersectionForLoading={observeIntersection!}
observeIntersectionForLoading={observeIntersectionForLoading!}
noAvatars={noAvatars}
canAutoLoad={canAutoLoad}
canAutoPlay={canAutoPlay}
@ -218,7 +232,7 @@ const WebPage: FC<OwnProps> = ({
{!inPreview && document && (
<Document
message={message}
observeIntersection={observeIntersection}
observeIntersection={observeIntersectionForLoading}
autoLoadFileMaxSizeMb={autoLoadFileMaxSizeMb}
onMediaClick={handleMediaClick}
onCancelUpload={onCancelMediaTransfer}
@ -226,14 +240,30 @@ const WebPage: FC<OwnProps> = ({
shouldWarnAboutSvg={shouldWarnAboutSvg}
/>
)}
{!inPreview && stickers && (
<div
ref={stickersRef}
className={buildClassName(
'media-inner', 'square-image', stickers.isEmoji && 'WebPage--emoji-grid', 'WebPage--stickers',
)}
>
{stickers.documents.map((sticker) => (
<div key={sticker.id} className="WebPage--sticker">
<StickerView
containerRef={stickersRef}
sticker={sticker}
shouldLoop
size={stickers.isEmoji ? EMOJI_SIZE : STICKER_SIZE}
customColor={customColor}
observeIntersectionForPlaying={observeIntersectionForPlaying}
observeIntersectionForLoading={observeIntersectionForLoading}
/>
</div>
))}
</div>
)}
{inPreview && displayUrl && !isArticle && (
<div className="WebPage-text">
{backgroundEmojiId && (
<EmojiIconBackground
emojiDocumentId={backgroundEmojiId}
className="WebPage--background-icons"
/>
)}
<p className="site-name">{displayUrl}</p>
<p className="site-description">{lang('Chat.Empty.LinkPreview')}</p>
</div>

View File

@ -29,6 +29,8 @@ export function getWebpageButtonText(type?: string) {
case 'telegram_channel_boost':
case 'telegram_group_boost':
return 'lng_view_button_boost';
case 'telegram_stickerset':
return 'lng_view_button_stickerset';
default:
return undefined;
}

View File

@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from '../lib/teact/teact';
import type { Scheduler } from '../util/schedulers';
import { type CallbackManager, createCallbackManager } from '../util/callbacks';
import {
debounce, throttle, throttleWith,
} from '../util/schedulers';
@ -16,7 +17,9 @@ export type ObserveFn = (target: HTMLElement, targetCallback?: TargetCallback) =
interface IntersectionController {
observer: IntersectionObserver;
callbacks: Map<HTMLElement, TargetCallback>;
addCallback: (element: HTMLElement, callback: TargetCallback) => void;
removeCallback: (element: HTMLElement, callback: TargetCallback) => void;
destroy: NoneToVoidFunction;
}
interface Response {
@ -78,14 +81,14 @@ export function useIntersectionObserver({
return () => {
if (controllerRef.current) {
controllerRef.current.observer.disconnect();
controllerRef.current.callbacks.clear();
controllerRef.current.destroy();
controllerRef.current = undefined;
}
};
}, [isDisabled]);
function initController() {
const callbacks = new Map();
const callbacks = new Map<HTMLElement, CallbackManager<TargetCallback>>();
const entriesAccumulator = new Map<Element, IntersectionObserverEntry>();
let observerCallback: typeof observerCallbackSync;
@ -108,10 +111,8 @@ export function useIntersectionObserver({
const entries = Array.from(entriesAccumulator.values());
entries.forEach((entry: IntersectionObserverEntry) => {
const callback = callbacks.get(entry.target);
if (callback) {
callback!(entry, entries);
}
const callbackManager = callbacks.get(entry.target as HTMLElement);
callbackManager?.runCallbacks(entry);
});
if (rootCallbackRef.current) {
@ -121,6 +122,25 @@ export function useIntersectionObserver({
entriesAccumulator.clear();
}
function addCallback(element: HTMLElement, callback: TargetCallback) {
if (!callbacks.get(element)) {
callbacks.set(element, createCallbackManager<TargetCallback>());
}
const callbackManager = callbacks.get(element)!;
callbackManager.addCallback(callback);
}
function removeCallback(element: HTMLElement, callback: TargetCallback) {
const callbackManager = callbacks.get(element);
if (!callbackManager) return;
callbackManager.removeCallback(callback);
if (!callbackManager.hasCallbacks()) {
callbacks.delete(element);
}
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
@ -140,7 +160,17 @@ export function useIntersectionObserver({
},
);
controllerRef.current = { observer, callbacks };
function destroy() {
callbacks.clear();
observer.disconnect();
}
controllerRef.current = {
observer,
addCallback,
removeCallback,
destroy,
};
}
const observe = useLastCallback((target, targetCallback) => {
@ -152,12 +182,12 @@ export function useIntersectionObserver({
controller.observer.observe(target);
if (targetCallback) {
controller.callbacks.set(target, targetCallback);
controller.addCallback(target, targetCallback);
}
return () => {
if (targetCallback) {
controller.callbacks.delete(target);
controller.removeCallback(target, targetCallback);
}
controller.observer.unobserve(target);