Web Page: Display stickerset preview (#4529)
This commit is contained in:
parent
1f834d42ed
commit
85bc6cb297
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user