Message: Support emoji-only shape for custom emojis (#2098)

This commit is contained in:
Alexander Zinchuk 2022-11-01 18:53:29 +01:00
parent 3bffde60cb
commit b1b09d9f41
30 changed files with 338 additions and 175 deletions

View File

@ -48,6 +48,7 @@ function buildEmojiSounds(appConfig: GramJsAppConfig) {
fileReference: Buffer.from(atob(l.file_reference_base64 fileReference: Buffer.from(atob(l.file_reference_base64
.replace(/-/g, '+') .replace(/-/g, '+')
.replace(/_/g, '/'))), .replace(/_/g, '/'))),
size: BigInt(0),
} as GramJs.Document); } as GramJs.Document);
acc[key] = l.id; acc[key] = l.id;

View File

@ -56,6 +56,7 @@ import { buildPeer } from '../gramjsBuilders';
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers'; import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
import { buildApiCallDiscardReason } from './calls'; import { buildApiCallDiscardReason } from './calls';
import parseEmojiOnlyString from '../../../util/parseEmojiOnlyString';
const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp'; const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp';
const INPUT_WAVEFORM_LENGTH = 63; const INPUT_WAVEFORM_LENGTH = 63;
@ -170,6 +171,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId); const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId);
const isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker); const isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker);
const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide; const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide;
const emojiOnlyCount = content.text && parseEmojiOnlyString(content.text.text);
return { return {
id: mtpMessage.id, id: mtpMessage.id,
@ -183,6 +185,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
isFromScheduled: mtpMessage.fromScheduled, isFromScheduled: mtpMessage.fromScheduled,
isSilent: mtpMessage.silent, isSilent: mtpMessage.silent,
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions), reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
...(emojiOnlyCount && { emojiOnlyCount }),
...(replyToMsgId && { replyToMessageId: replyToMsgId }), ...(replyToMsgId && { replyToMessageId: replyToMsgId }),
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }), ...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
...(replyToTopId && { replyToTopMessageId: replyToTopId }), ...(replyToTopId && { replyToTopMessageId: replyToTopId }),
@ -1197,6 +1200,7 @@ export function buildLocalMessage(
const localId = getNextLocalMessageId(); const localId = getNextLocalMessageId();
const media = attachment && buildUploadingMedia(attachment); const media = attachment && buildUploadingMedia(attachment);
const isChannel = chat.type === 'chatTypeChannel'; const isChannel = chat.type === 'chatTypeChannel';
const emojiOnlyCount = text && parseEmojiOnlyString(text);
return { return {
id: localId, id: localId,
@ -1217,6 +1221,7 @@ export function buildLocalMessage(
date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset, date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset,
isOutgoing: !isChannel, isOutgoing: !isChannel,
senderId: sendAs?.id || currentUserId, senderId: sendAs?.id || currentUserId,
...(emojiOnlyCount && { emojiOnlyCount }),
...(replyingTo && { replyToMessageId: replyingTo }), ...(replyingTo && { replyToMessageId: replyingTo }),
...(replyingToTopId && { replyToTopMessageId: replyingToTopId }), ...(replyingToTopId && { replyToTopMessageId: replyingToTopId }),
...(groupedId && { ...(groupedId && {
@ -1256,6 +1261,7 @@ export function buildLocalForwardedMessage(
text: content.text.text, text: content.text.text,
entities: content.text.entities?.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji), entities: content.text.entities?.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji),
} : content.text; } : content.text;
const emojiOnlyCount = content.text && parseEmojiOnlyString(content.text.text);
const updatedContent = { const updatedContent = {
...content, ...content,
@ -1272,6 +1278,7 @@ export function buildLocalForwardedMessage(
sendingState: 'messageSendingStatePending', sendingState: 'messageSendingStatePending',
groupedId, groupedId,
isInAlbum, isInAlbum,
...(emojiOnlyCount && { emojiOnlyCount }),
// Forward info doesn't get added when users forwards his own messages, also when forwarding audio // Forward info doesn't get added when users forwards his own messages, also when forwarding audio
...(senderId !== currentUserId && !isAudio && !noAuthors && { ...(senderId !== currentUserId && !isAudio && !noAuthors && {
forwardInfo: { forwardInfo: {

View File

@ -401,6 +401,7 @@ export interface ApiMessage {
isProtected?: boolean; isProtected?: boolean;
transcriptionId?: string; transcriptionId?: string;
isTranscriptionError?: boolean; isTranscriptionError?: boolean;
emojiOnlyCount?: number;
reactors?: { reactors?: {
nextOffset?: string; nextOffset?: string;
count: number; count: number;

View File

@ -20,7 +20,7 @@ function AnimatedIconFromSticker(props: OwnProps) {
} = props; } = props;
const thumbDataUri = sticker?.thumbnail?.dataUri; const thumbDataUri = sticker?.thumbnail?.dataUri;
const localMediaHash = `sticker${sticker?.id}`; const localMediaHash = sticker && `sticker${sticker.id}`;
const previewBlobUrl = useMedia( const previewBlobUrl = useMedia(
sticker ? getStickerPreviewHash(sticker.id) : undefined, sticker ? getStickerPreviewHash(sticker.id) : undefined,
noLoad && !forcePreview, noLoad && !forcePreview,

View File

@ -5,14 +5,11 @@
height: var(--custom-emoji-size); height: var(--custom-emoji-size);
position: relative; position: relative;
border-radius: 0 !important;
&.with-grid-fix .media, &.with-grid-fix .thumb { &.with-grid-fix .media, &.with-grid-fix .thumb {
width: calc(100% + 1px) !important; width: calc(100% + 1px) !important;
height: calc(100% + 1px) !important; height: calc(100% + 1px) !important;
vertical-align: baseline;
}
:global(.emoji-small) {
vertical-align: baseline !important; // Fix for fallback on Windows, when custom emoji not ready
} }
&:global(.custom-color) { &:global(.custom-color) {
@ -29,8 +26,10 @@
width: var(--custom-emoji-size) !important; width: var(--custom-emoji-size) !important;
height: var(--custom-emoji-size) !important; height: var(--custom-emoji-size) !important;
border-radius: 0 !important;
:global(canvas) { :global(canvas) {
width: var(--custom-emoji-size) !important; width: 100% !important;
height: var(--custom-emoji-size) !important; height: 100% !important;
} }
} }

View File

@ -21,13 +21,16 @@ import styles from './CustomEmoji.module.scss';
import svgPlaceholder from '../../assets/square.svg'; import svgPlaceholder from '../../assets/square.svg';
type OwnProps = { type OwnProps = {
ref?: React.RefObject<HTMLDivElement>;
documentId: string; documentId: string;
children?: TeactNode; children?: TeactNode;
size?: number; size?: number;
className?: string; className?: string;
loopLimit?: number; loopLimit?: number;
style?: string;
withGridFix?: boolean; withGridFix?: boolean;
shouldPreloadPreview?: boolean; shouldPreloadPreview?: boolean;
forceOnHeavyAnimation?: boolean;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
onClick?: NoneToVoidFunction; onClick?: NoneToVoidFunction;
@ -36,18 +39,24 @@ type OwnProps = {
const STICKER_SIZE = 24; const STICKER_SIZE = 24;
const CustomEmoji: FC<OwnProps> = ({ const CustomEmoji: FC<OwnProps> = ({
ref,
documentId, documentId,
size = STICKER_SIZE, size = STICKER_SIZE,
className, className,
loopLimit, loopLimit,
style,
withGridFix, withGridFix,
shouldPreloadPreview, shouldPreloadPreview,
forceOnHeavyAnimation,
observeIntersection, observeIntersection,
observeIntersectionForPlaying, observeIntersectionForPlaying,
onClick, onClick,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); let containerRef = useRef<HTMLDivElement>(null);
if (ref) {
containerRef = ref;
}
// An alternative to `withGlobal` to avoid adding numerous global containers // An alternative to `withGlobal` to avoid adding numerous global containers
const customEmoji = useCustomEmoji(documentId); const customEmoji = useCustomEmoji(documentId);
@ -60,11 +69,11 @@ const CustomEmoji: FC<OwnProps> = ({
const hasCustomColor = customEmoji && selectIsDefaultEmojiStatusPack(getGlobal(), customEmoji.stickerSetInfo); const hasCustomColor = customEmoji && selectIsDefaultEmojiStatusPack(getGlobal(), customEmoji.stickerSetInfo);
useEffect(() => { useEffect(() => {
if (!hasCustomColor || !ref.current) { if (!hasCustomColor || !containerRef.current) {
setCustomColor(undefined); setCustomColor(undefined);
return; return;
} }
const hexColor = getPropertyHexColor(getComputedStyle(ref.current), '--emoji-status-color'); const hexColor = getPropertyHexColor(getComputedStyle(containerRef.current), '--emoji-status-color');
if (!hexColor) { if (!hexColor) {
setCustomColor(undefined); setCustomColor(undefined);
return; return;
@ -100,7 +109,7 @@ const CustomEmoji: FC<OwnProps> = ({
return ( return (
<div <div
ref={ref} ref={containerRef}
className={buildClassName( className={buildClassName(
styles.root, styles.root,
className, className,
@ -110,12 +119,13 @@ const CustomEmoji: FC<OwnProps> = ({
withGridFix && styles.withGridFix, withGridFix && styles.withGridFix,
)} )}
onClick={onClick} onClick={onClick}
style={style}
> >
{!customEmoji ? ( {!customEmoji ? (
<img className={styles.thumb} src={svgPlaceholder} alt="Emoji" /> <img className={styles.thumb} src={svgPlaceholder} alt="Emoji" />
) : ( ) : (
<StickerView <StickerView
containerRef={ref} containerRef={containerRef}
sticker={customEmoji} sticker={customEmoji}
isSmall isSmall
size={size} size={size}
@ -126,6 +136,7 @@ const CustomEmoji: FC<OwnProps> = ({
loopLimit={loopLimit} loopLimit={loopLimit}
shouldPreloadPreview={shouldPreloadPreview} shouldPreloadPreview={shouldPreloadPreview}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
forceOnHeavyAnimation={forceOnHeavyAnimation}
observeIntersectionForPlaying={observeIntersectionForPlaying} observeIntersectionForPlaying={observeIntersectionForPlaying}
onVideoEnded={handleVideoEnded} onVideoEnded={handleVideoEnded}
onAnimatedStickerLoop={handleStickerLoop} onAnimatedStickerLoop={handleStickerLoop}

View File

@ -1,6 +1,6 @@
.DotAnimation { .DotAnimation {
display: inline-flex; display: inline-flex;
align-items: baseline; align-items: center;
.ellipsis { .ellipsis {
display: flex; display: flex;

View File

@ -1,8 +1,11 @@
import type { FC } from '../../lib/teact/teact';
import React from '../../lib/teact/teact'; import React from '../../lib/teact/teact';
import useLang from '../../hooks/useLang'; import type { FC } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import renderText from './helpers/renderText';
import useLang from '../../hooks/useLang';
import './DotAnimation.scss'; import './DotAnimation.scss';
@ -15,7 +18,7 @@ const DotAnimation: FC<OwnProps> = ({ content, className }) => {
const lang = useLang(); const lang = useLang();
return ( return (
<span className={buildClassName('DotAnimation', className)} dir={lang.isRtl ? 'rtl' : 'auto'}> <span className={buildClassName('DotAnimation', className)} dir={lang.isRtl ? 'rtl' : 'auto'}>
{content} {renderText(content)}
<span className="ellipsis" /> <span className="ellipsis" />
</span> </span>
); );

View File

@ -1,8 +1,7 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { FC } from '../../lib/teact/teact';
import React, { useEffect, useCallback, memo } from '../../lib/teact/teact'; import React, { useEffect, useCallback, memo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
import type { ApiUser, ApiTypingStatus, ApiUserStatus } from '../../api/types'; import type { ApiUser, ApiTypingStatus, ApiUserStatus } from '../../api/types';
import type { GlobalState } from '../../global/types'; import type { GlobalState } from '../../global/types';
import type { AnimationLevel } from '../../types'; import type { AnimationLevel } from '../../types';
@ -10,6 +9,9 @@ import { MediaViewerOrigin } from '../../types';
import { selectChatMessages, selectUser, selectUserStatus } from '../../global/selectors'; import { selectChatMessages, selectUser, selectUserStatus } from '../../global/selectors';
import { getUserStatus, isUserOnline } from '../../global/helpers'; import { getUserStatus, isUserOnline } from '../../global/helpers';
import buildClassName from '../../util/buildClassName';
import renderText from './helpers/renderText';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import Avatar from './Avatar'; import Avatar from './Avatar';
@ -79,7 +81,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
} }
}, [userId, loadFullUser, loadProfilePhotos, lastSyncTime, withFullInfo, withMediaViewer]); }, [userId, loadFullUser, loadProfilePhotos, lastSyncTime, withFullInfo, withMediaViewer]);
const handleAvatarViewerOpen = useCallback((e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => { const handleAvatarViewerOpen = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {
if (user && hasMedia) { if (user && hasMedia) {
e.stopPropagation(); e.stopPropagation();
openMediaViewer({ openMediaViewer({
@ -101,7 +103,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
return withDots ? ( return withDots ? (
<DotAnimation className="status" content={status} /> <DotAnimation className="status" content={status} />
) : ( ) : (
<span className="status" dir="auto">{status}</span> <span className="status" dir="auto">{renderText(status)}</span>
); );
} }
@ -120,7 +122,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
} }
return ( return (
<span className={`status ${isUserOnline(user, userStatus) ? 'online' : ''}`}> <span className={buildClassName('status', isUserOnline(user, userStatus) && 'online')}>
{withUsername && user.username && <span className="handle">{user.username}</span>} {withUsername && user.username && <span className="handle">{user.username}</span>}
<span className="user-status" dir="auto">{getUserStatus(lang, user, userStatus, serverTimeOffset)}</span> <span className="user-status" dir="auto">{getUserStatus(lang, user, userStatus, serverTimeOffset)}</span>
</span> </span>

View File

@ -9,6 +9,8 @@
.media { .media {
position: absolute; position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

View File

@ -34,6 +34,7 @@ type OwnProps = {
loopLimit?: number; loopLimit?: number;
shouldLoop?: boolean; shouldLoop?: boolean;
shouldPreloadPreview?: boolean; shouldPreloadPreview?: boolean;
forceOnHeavyAnimation?: boolean;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
noLoad?: boolean; noLoad?: boolean;
@ -57,6 +58,7 @@ const StickerView: FC<OwnProps> = ({
loopLimit, loopLimit,
shouldLoop = false, shouldLoop = false,
shouldPreloadPreview, shouldPreloadPreview,
forceOnHeavyAnimation,
observeIntersection, observeIntersection,
observeIntersectionForPlaying, observeIntersectionForPlaying,
noLoad, noLoad,
@ -118,6 +120,7 @@ const StickerView: FC<OwnProps> = ({
play={shouldPlay} play={shouldPlay}
color={customColor} color={customColor}
noLoop={!shouldLoop} noLoop={!shouldLoop}
forceOnHeavyAnimation={forceOnHeavyAnimation}
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)} isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
onLoad={markPlayerReady} onLoad={markPlayerReady}
onLoop={onAnimatedStickerLoop} onLoop={onAnimatedStickerLoop}

View File

@ -1,30 +0,0 @@
import twemojiRegex from '../../../lib/twemojiRegex';
const DETECT_UP_TO = 3;
const MAX_LENGTH = DETECT_UP_TO * 8; // Maximum 8 per one emoji.
const RE_EMOJI_ONLY = new RegExp(`^(?:${twemojiRegex.source})+$`, '');
const parseEmojiOnlyString = (text: string): number | false => {
if (text.length > MAX_LENGTH) {
return false;
}
const isEmojiOnly = Boolean(text.match(RE_EMOJI_ONLY));
if (!isEmojiOnly) {
return false;
}
let emojiCount = 0;
while (twemojiRegex.exec(text)) {
emojiCount++;
if (emojiCount > DETECT_UP_TO) {
twemojiRegex.lastIndex = 0;
return false;
}
}
return emojiCount;
};
export default parseEmojiOnlyString;

View File

@ -18,7 +18,7 @@ import trimText from '../../../util/trimText';
export function renderMessageText( export function renderMessageText(
message: ApiMessage, message: ApiMessage,
highlight?: string, highlight?: string,
shouldRenderHqEmoji?: boolean, emojiSize?: number,
isSimple?: boolean, isSimple?: boolean,
truncateLength?: number, truncateLength?: number,
isProtected?: boolean, isProtected?: boolean,
@ -35,7 +35,7 @@ export function renderMessageText(
trimText(text, truncateLength), trimText(text, truncateLength),
entities, entities,
highlight, highlight,
shouldRenderHqEmoji, emojiSize,
undefined, undefined,
message.id, message.id,
isSimple, isSimple,

View File

@ -25,11 +25,13 @@ interface IOrganizedEntity {
nestedEntities: IOrganizedEntity[]; nestedEntities: IOrganizedEntity[];
} }
const HQ_EMOJI_THRESHOLD = 64;
export function renderTextWithEntities( export function renderTextWithEntities(
text: string, text: string,
entities?: ApiMessageEntity[], entities?: ApiMessageEntity[],
highlight?: string, highlight?: string,
shouldRenderHqEmoji?: boolean, emojiSize?: number,
shouldRenderAsHtml?: boolean, shouldRenderAsHtml?: boolean,
messageId?: number, messageId?: number,
isSimple?: boolean, isSimple?: boolean,
@ -37,7 +39,7 @@ export function renderTextWithEntities(
observeIntersection?: ObserveFn, observeIntersection?: ObserveFn,
) { ) {
if (!entities || !entities.length) { if (!entities || !entities.length) {
return renderMessagePart(text, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple); return renderMessagePart(text, highlight, emojiSize, shouldRenderAsHtml, isSimple);
} }
const result: TextPart[] = []; const result: TextPart[] = [];
@ -66,7 +68,7 @@ export function renderTextWithEntities(
} }
if (textBefore) { if (textBefore) {
renderResult.push(...renderMessagePart( renderResult.push(...renderMessagePart(
textBefore, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple, textBefore, highlight, emojiSize, shouldRenderAsHtml, isSimple,
) as TextPart[]); ) as TextPart[]);
} }
} }
@ -110,7 +112,15 @@ export function renderTextWithEntities(
const newEntity = shouldRenderAsHtml const newEntity = shouldRenderAsHtml
? processEntityAsHtml(entity, entityContent, nestedEntityContent) ? processEntityAsHtml(entity, entityContent, nestedEntityContent)
: processEntity( : processEntity(
entity, entityContent, nestedEntityContent, highlight, messageId, isSimple, isProtected, observeIntersection, entity,
entityContent,
nestedEntityContent,
highlight,
messageId,
isSimple,
isProtected,
observeIntersection,
emojiSize,
); );
if (Array.isArray(newEntity)) { if (Array.isArray(newEntity)) {
@ -128,7 +138,7 @@ export function renderTextWithEntities(
} }
if (textAfter) { if (textAfter) {
renderResult.push(...renderMessagePart( renderResult.push(...renderMessagePart(
textAfter, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple, textAfter, highlight, emojiSize, shouldRenderAsHtml, isSimple,
) as TextPart[]); ) as TextPart[]);
} }
} }
@ -181,7 +191,7 @@ export function getTextWithEntitiesAsHtml(formattedText?: ApiFormattedText) {
function renderMessagePart( function renderMessagePart(
content: TextPart | TextPart[], content: TextPart | TextPart[],
highlight?: string, highlight?: string,
shouldRenderHqEmoji?: boolean, emojiSize?: number,
shouldRenderAsHtml?: boolean, shouldRenderAsHtml?: boolean,
isSimple?: boolean, isSimple?: boolean,
) { ) {
@ -189,7 +199,7 @@ function renderMessagePart(
const result: TextPart[] = []; const result: TextPart[] = [];
content.forEach((c) => { content.forEach((c) => {
result.push(...renderMessagePart(c, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple)); result.push(...renderMessagePart(c, highlight, emojiSize, shouldRenderAsHtml, isSimple));
}); });
return result; return result;
@ -199,7 +209,7 @@ function renderMessagePart(
return renderText(content, ['escape_html', 'emoji_html', 'br_html']); return renderText(content, ['escape_html', 'emoji_html', 'br_html']);
} }
const emojiFilter = shouldRenderHqEmoji ? 'hq_emoji' : 'emoji'; const emojiFilter = emojiSize && emojiSize > HQ_EMOJI_THRESHOLD ? 'hq_emoji' : 'emoji';
const filters: TextFilter[] = [emojiFilter]; const filters: TextFilter[] = [emojiFilter];
if (!isSimple) { if (!isSimple) {
@ -288,6 +298,7 @@ function processEntity(
isSimple?: boolean, isSimple?: boolean,
isProtected?: boolean, isProtected?: boolean,
observeIntersection?: ObserveFn, observeIntersection?: ObserveFn,
emojiSize?: number,
) { ) {
const entityText = typeof entityContent === 'string' && entityContent; const entityText = typeof entityContent === 'string' && entityContent;
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent; const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
@ -310,7 +321,12 @@ function processEntity(
if (entity.type === ApiMessageEntityTypes.CustomEmoji) { if (entity.type === ApiMessageEntityTypes.CustomEmoji) {
return ( return (
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix /> <CustomEmoji
documentId={entity.documentId}
observeIntersection={observeIntersection}
withGridFix={!emojiSize}
size={emojiSize}
/>
); );
} }
return text; return text;
@ -418,7 +434,12 @@ function processEntity(
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>; return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
case ApiMessageEntityTypes.CustomEmoji: case ApiMessageEntityTypes.CustomEmoji:
return ( return (
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix /> <CustomEmoji
documentId={entity.documentId}
observeIntersection={observeIntersection}
withGridFix={!emojiSize}
size={emojiSize}
/>
); );
default: default:
return renderNestedMessagePart(); return renderNestedMessagePart();

View File

@ -1,11 +1,15 @@
import { import {
useCallback, useEffect, useRef, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import safePlay from '../../../util/safePlay';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import useMedia from '../../../hooks/useMedia';
import type { ActiveEmojiInteraction } from '../../../global/types'; import type { ActiveEmojiInteraction } from '../../../global/types';
import safePlay from '../../../util/safePlay';
import { selectLocalAnimatedEmojiEffectByName } from '../../../global/selectors'; import { selectLocalAnimatedEmojiEffectByName } from '../../../global/selectors';
import buildStyle from '../../../util/buildStyle';
import useMedia from '../../../hooks/useMedia';
const SIZE = 104; const SIZE = 104;
const INTERACTION_BUNCH_TIME = 1000; const INTERACTION_BUNCH_TIME = 1000;
@ -20,6 +24,7 @@ export default function useAnimatedEmoji(
isOwn?: boolean, isOwn?: boolean,
localEffect?: string, localEffect?: string,
emoji?: string, emoji?: string,
preferredSize?: number,
) { ) {
const { const {
interactWithAnimatedEmoji, sendEmojiInteraction, sendWatchingEmojiInteraction, interactWithAnimatedEmoji, sendEmojiInteraction, sendWatchingEmojiInteraction,
@ -35,7 +40,8 @@ export default function useAnimatedEmoji(
const soundMediaData = useMedia(soundId ? `document${soundId}` : undefined, !soundId); const soundMediaData = useMedia(soundId ? `document${soundId}` : undefined, !soundId);
const style = `width: ${SIZE}px; height: ${SIZE}px;`; const size = preferredSize || SIZE;
const style = buildStyle(`width: ${size}px`, `height: ${size}px`, (emoji || localEffect) && 'cursor: pointer');
const interactions = useRef<number[] | undefined>(undefined); const interactions = useRef<number[] | undefined>(undefined);
const startedInteractions = useRef<number | undefined>(undefined); const startedInteractions = useRef<number | undefined>(undefined);
@ -87,7 +93,7 @@ export default function useAnimatedEmoji(
emoji, emoji,
x, x,
y, y,
startSize: SIZE, startSize: size,
isReversed: !isOwn, isReversed: !isOwn,
}); });
@ -102,7 +108,7 @@ export default function useAnimatedEmoji(
: TIME_DEFAULT); : TIME_DEFAULT);
}, [ }, [
chatId, emoji, hasEffect, interactWithAnimatedEmoji, isOwn, chatId, emoji, hasEffect, interactWithAnimatedEmoji, isOwn,
localEffect, messageId, play, sendInteractionBunch, localEffect, messageId, play, sendInteractionBunch, size,
]); ]);
// Set an end anchor for remote activated interaction // Set an end anchor for remote activated interaction
@ -126,7 +132,7 @@ export default function useAnimatedEmoji(
id, id,
chatId, chatId,
emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect) : emoji, emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect) : emoji,
startSize: SIZE, startSize: size,
x, x,
y, y,
isReversed: !isOwn, isReversed: !isOwn,
@ -134,12 +140,12 @@ export default function useAnimatedEmoji(
play(); play();
}); });
}, [ }, [
activeEmojiInteractions, chatId, emoji, isOwn, localEffect, messageId, play, sendWatchingEmojiInteraction, activeEmojiInteractions, chatId, emoji, isOwn, localEffect, messageId, play, sendWatchingEmojiInteraction, size,
]); ]);
return { return {
ref, ref,
size: SIZE, size,
style, style,
handleClick, handleClick,
}; };

View File

@ -83,7 +83,8 @@ const EmojiInteractionAnimation: FC<OwnProps & StateProps> = ({
}, PLAYING_DURATION); }, PLAYING_DURATION);
}, [stop]); }, [stop]);
const effectTgsUrl = useMedia(`sticker${effectAnimationId}`, !effectAnimationId); const effectHash = effectAnimationId && `sticker${effectAnimationId}`;
const effectTgsUrl = useMedia(effectHash, !effectAnimationId);
if (!activeEmojiInteraction.startSize) { if (!activeEmojiInteraction.startSize) {
return undefined; return undefined;

View File

@ -20,7 +20,7 @@ import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevD
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck'; import { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import parseEmojiOnlyString from '../../common/helpers/parseEmojiOnlyString'; import parseEmojiOnlyString from '../../../util/parseEmojiOnlyString';
import { isSelectionInsideInput } from './helpers/selection'; import { isSelectionInsideInput } from './helpers/selection';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';

View File

@ -1,5 +1,9 @@
import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types'; import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types';
import { EMOJI_SIZES } from '../../../../config';
import { REM } from '../../../common/helpers/mediaDimensions';
import { getCustomEmojiPreviewMediaData } from '../../../../util/customEmojiManager'; import { getCustomEmojiPreviewMediaData } from '../../../../util/customEmojiManager';
import placeholderSrc from '../../../../assets/square.svg'; import placeholderSrc from '../../../../assets/square.svg';
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]'; export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
@ -27,3 +31,10 @@ export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessa
src="${mediaData || placeholderSrc}" src="${mediaData || placeholderSrc}"
/>`; />`;
} }
export function getCustomEmojiSize(maxEmojisInLine: number): number | undefined {
if (maxEmojisInLine > EMOJI_SIZES) return undefined;
const size = (6 - (maxEmojisInLine * 0.625)) * REM; // Should be the same as in _message-content.scss
return size;
}

View File

@ -5,7 +5,7 @@ import type { ApiSticker } from '../../../../api/types';
import { EMOJI_IMG_REGEX } from '../../../../config'; import { EMOJI_IMG_REGEX } from '../../../../config';
import { IS_EMOJI_SUPPORTED } from '../../../../util/environment'; import { IS_EMOJI_SUPPORTED } from '../../../../util/environment';
import parseEmojiOnlyString from '../../../common/helpers/parseEmojiOnlyString'; import parseEmojiOnlyString from '../../../../util/parseEmojiOnlyString';
import { prepareForRegExp } from '../helpers/prepareForRegExp'; import { prepareForRegExp } from '../helpers/prepareForRegExp';
const STARTS_ENDS_ON_EMOJI_IMG_REGEX = new RegExp(`^${EMOJI_IMG_REGEX.source}$`, 'g'); const STARTS_ENDS_ON_EMOJI_IMG_REGEX = new RegExp(`^${EMOJI_IMG_REGEX.source}$`, 'g');

View File

@ -3,8 +3,8 @@ import {
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
MOBILE_SCREEN_MAX_WIDTH, MOBILE_SCREEN_MAX_WIDTH,
} from '../../../config'; } from '../../../config';
import { REM } from '../../common/helpers/mediaDimensions';
const REM = 16; // px
const MAX_TOOLBAR_WIDTH = 32 * REM; const MAX_TOOLBAR_WIDTH = 32 * REM;
const MAX_MESSAGES_LIST_WIDTH = 45.5 * REM; const MAX_MESSAGES_LIST_WIDTH = 45.5 * REM;
export const SIDE_COLUMN_MAX_WIDTH = 26.5 * REM; export const SIDE_COLUMN_MAX_WIDTH = 26.5 * REM;

View File

@ -0,0 +1,82 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global';
import type { ApiSticker } from '../../../api/types';
import type { ActiveEmojiInteraction } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { LIKE_STICKER_ID } from '../../common/helpers/mediaDimensions';
import {
selectAnimatedEmojiEffect,
selectAnimatedEmojiSound,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
import useAnimatedEmoji from '../../common/hooks/useAnimatedEmoji';
import CustomEmoji from '../../common/CustomEmoji';
import './AnimatedEmoji.scss';
type OwnProps = {
customEmojiId: string;
withEffects: boolean;
isOwn?: boolean;
size?: 'large' | 'medium' | 'small';
lastSyncTime?: number;
forceLoadPreview?: boolean;
messageId?: number;
chatId?: string;
activeEmojiInteractions?: ActiveEmojiInteraction[];
observeIntersection?: ObserveFn;
};
interface StateProps {
sticker?: ApiSticker;
effect?: ApiSticker;
soundId?: string;
}
const AnimatedCustomEmoji: FC<OwnProps & StateProps> = ({
isOwn,
customEmojiId,
messageId,
chatId,
activeEmojiInteractions,
sticker,
effect,
soundId,
observeIntersection,
}) => {
const {
ref,
size,
style,
handleClick,
} = useAnimatedEmoji(
chatId, messageId, soundId, activeEmojiInteractions, isOwn, undefined, effect?.emoji, getCustomEmojiSize(1),
);
return (
<CustomEmoji
ref={ref}
documentId={customEmojiId}
size={size}
forceOnHeavyAnimation
observeIntersection={observeIntersection}
className={buildClassName('AnimatedEmoji media-inner', sticker?.id === LIKE_STICKER_ID && 'like-sticker-thumb')}
onClick={handleClick}
style={style}
/>
);
};
export default memo(withGlobal<OwnProps>((global, { customEmojiId, withEffects }) => {
const sticker = global.customEmojis.byId[customEmojiId];
return {
sticker,
effect: sticker?.emoji && withEffects ? selectAnimatedEmojiEffect(global, sticker.emoji) : undefined,
soundId: sticker?.emoji && selectAnimatedEmojiSound(global, sticker.emoji),
};
})(AnimatedCustomEmoji));

View File

@ -1,6 +1,4 @@
.AnimatedEmoji { .AnimatedEmoji {
margin-bottom: 0.75rem;
&.like-sticker-thumb img { &.like-sticker-thumb img {
transform: scale(0.8); transform: scale(0.8);
} }

View File

@ -67,19 +67,20 @@ import {
isChatWithRepliesBot, isChatWithRepliesBot,
getMessageCustomShape, getMessageCustomShape,
isChatChannel, isChatChannel,
getMessageSingleEmoji, getMessageSingleRegularEmoji,
getSenderTitle, getSenderTitle,
getUserColorKey, getUserColorKey,
areReactionsEmpty, areReactionsEmpty,
getMessageHtmlId, getMessageHtmlId,
isGeoLiveExpired, isGeoLiveExpired,
getMessageSingleCustomEmoji,
} from '../../../global/helpers'; } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import { renderMessageText } from '../../common/helpers/renderMessageText'; import { renderMessageText } from '../../common/helpers/renderMessageText';
import { ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions'; import { ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions';
import { buildContentClassName, isEmojiOnlyMessage } from './helpers/buildContentClassName'; import { buildContentClassName } from './helpers/buildContentClassName';
import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions'; import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions';
import { calculateAlbumLayout } from './helpers/calculateAlbumLayout'; import { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
@ -94,6 +95,7 @@ import useOuterHandlers from './hooks/useOuterHandlers';
import useInnerHandlers from './hooks/useInnerHandlers'; import useInnerHandlers from './hooks/useInnerHandlers';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { isElementInViewport } from '../../../util/isElementInViewport'; import { isElementInViewport } from '../../../util/isElementInViewport';
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
@ -104,6 +106,7 @@ import MessageMeta from './MessageMeta';
import ContextMenuContainer from './ContextMenuContainer.async'; import ContextMenuContainer from './ContextMenuContainer.async';
import Sticker from './Sticker'; import Sticker from './Sticker';
import AnimatedEmoji from './AnimatedEmoji'; import AnimatedEmoji from './AnimatedEmoji';
import AnimatedCustomEmoji from './AnimatedCustomEmoji';
import Photo from './Photo'; import Photo from './Photo';
import Video from './Video'; import Video from './Video';
import Contact from './Contact'; import Contact from './Contact';
@ -182,6 +185,7 @@ type StateProps = {
serverTimeOffset: number; serverTimeOffset: number;
highlight?: string; highlight?: string;
animatedEmoji?: string; animatedEmoji?: string;
animatedCustomEmoji?: string;
isInSelectMode?: boolean; isInSelectMode?: boolean;
isSelected?: boolean; isSelected?: boolean;
isGroupSelected?: boolean; isGroupSelected?: boolean;
@ -272,6 +276,7 @@ const Message: FC<OwnProps & StateProps> = ({
serverTimeOffset, serverTimeOffset,
highlight, highlight,
animatedEmoji, animatedEmoji,
animatedCustomEmoji,
isInSelectMode, isInSelectMode,
isSelected, isSelected,
isGroupSelected, isGroupSelected,
@ -348,7 +353,7 @@ const Message: FC<OwnProps & StateProps> = ({
const hasReply = isReplyMessage(message) && !shouldHideReply; const hasReply = isReplyMessage(message) && !shouldHideReply;
const hasThread = Boolean(threadInfo) && messageListType === 'thread'; const hasThread = Boolean(threadInfo) && messageListType === 'thread';
const customShape = getMessageCustomShape(message); const customShape = getMessageCustomShape(message);
const hasAnimatedEmoji = animatedEmoji; const hasAnimatedEmoji = animatedEmoji || animatedCustomEmoji;
const hasReactions = reactionMessage?.reactions && !areReactionsEmpty(reactionMessage.reactions); const hasReactions = reactionMessage?.reactions && !areReactionsEmpty(reactionMessage.reactions);
const asForwarded = ( const asForwarded = (
forwardInfo forwardInfo
@ -513,10 +518,11 @@ const Message: FC<OwnProps & StateProps> = ({
}); });
const withAppendix = contentClassName.includes('has-appendix'); const withAppendix = contentClassName.includes('has-appendix');
const emojiSize = message.emojiOnlyCount && getCustomEmojiSize(message.emojiOnlyCount);
const textParts = renderMessageText( const textParts = renderMessageText(
message, message,
highlight, highlight,
isEmojiOnlyMessage(customShape), emojiSize,
undefined, undefined,
undefined, undefined,
isProtected, isProtected,
@ -528,7 +534,7 @@ const Message: FC<OwnProps & StateProps> = ({
metaPosition = 'none'; metaPosition = 'none';
} else if (isInDocumentGroupNotLast) { } else if (isInDocumentGroupNotLast) {
metaPosition = 'none'; metaPosition = 'none';
} else if (textParts && !hasAnimatedEmoji && !webPage) { } else if (textParts && !webPage && !hasAnimatedEmoji) {
metaPosition = 'in-text'; metaPosition = 'in-text';
} else { } else {
metaPosition = 'standalone'; metaPosition = 'standalone';
@ -538,7 +544,7 @@ const Message: FC<OwnProps & StateProps> = ({
if (areReactionsInMeta) { if (areReactionsInMeta) {
reactionsPosition = 'in-meta'; reactionsPosition = 'in-meta';
} else if (hasReactions) { } else if (hasReactions) {
if (customShape || ((photo || video || hasAnimatedEmoji) && !textParts)) { if (customShape || ((photo || video) && !textParts)) {
reactionsPosition = 'outside'; reactionsPosition = 'outside';
} else if (asForwarded) { } else if (asForwarded) {
metaPosition = 'standalone'; metaPosition = 'standalone';
@ -700,6 +706,19 @@ const Message: FC<OwnProps & StateProps> = ({
onStopEffect={stopStickerEffect} onStopEffect={stopStickerEffect}
/> />
)} )}
{animatedCustomEmoji && (
<AnimatedCustomEmoji
customEmojiId={animatedCustomEmoji}
withEffects={isUserId(chatId)}
isOwn={isOwn}
observeIntersection={observeIntersectionForMedia}
lastSyncTime={lastSyncTime}
forceLoadPreview={isLocal}
messageId={messageId}
chatId={chatId}
activeEmojiInteractions={activeEmojiInteractions}
/>
)}
{animatedEmoji && ( {animatedEmoji && (
<AnimatedEmoji <AnimatedEmoji
emoji={animatedEmoji} emoji={animatedEmoji}
@ -1114,10 +1133,11 @@ export default memo(withGlobal<OwnProps>(
const { query: highlight } = selectCurrentTextSearch(global) || {}; const { query: highlight } = selectCurrentTextSearch(global) || {};
const singleEmoji = getMessageSingleEmoji(message); const singleEmoji = getMessageSingleRegularEmoji(message);
const animatedEmoji = singleEmoji && ( const animatedEmoji = singleEmoji && (
selectAnimatedEmoji(global, singleEmoji) || selectLocalAnimatedEmoji(global, singleEmoji) selectAnimatedEmoji(global, singleEmoji) || selectLocalAnimatedEmoji(global, singleEmoji)
) ? singleEmoji : undefined; ) ? singleEmoji : undefined;
const animatedCustomEmoji = getMessageSingleCustomEmoji(message);
let isSelected: boolean; let isSelected: boolean;
if (album?.messages) { if (album?.messages) {
@ -1167,6 +1187,7 @@ export default memo(withGlobal<OwnProps>(
serverTimeOffset, serverTimeOffset,
highlight, highlight,
animatedEmoji, animatedEmoji,
animatedCustomEmoji,
isInSelectMode: selectIsInSelectMode(global), isInSelectMode: selectIsInSelectMode(global),
isSelected, isSelected,
isGroupSelected: ( isGroupSelected: (

View File

@ -36,8 +36,12 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
const availableReaction = availableReactions?.find((r) => r.reaction === reaction); const availableReaction = availableReactions?.find((r) => r.reaction === reaction);
const centerIconId = availableReaction?.centerIcon?.id; const centerIconId = availableReaction?.centerIcon?.id;
const effectId = availableReaction?.aroundAnimation?.id; const effectId = availableReaction?.aroundAnimation?.id;
const mediaDataCenterIcon = useMedia(`sticker${centerIconId}`, !centerIconId);
const mediaDataEffect = useMedia(`sticker${effectId}`, !effectId); const mediaHashCenterIcon = centerIconId && `sticker${centerIconId}`;
const mediaHashEffect = effectId && `sticker${effectId}`;
const mediaDataCenterIcon = useMedia(mediaHashCenterIcon, !centerIconId);
const mediaDataEffect = useMedia(mediaHashEffect, !effectId);
const shouldPlay = Boolean(activeReaction?.reaction === reaction && mediaDataCenterIcon && mediaDataEffect); const shouldPlay = Boolean(activeReaction?.reaction === reaction && mediaDataCenterIcon && mediaDataEffect);
const { const {

View File

@ -13,9 +13,11 @@
&.has-action-button { &.has-action-button {
max-width: min(29rem, calc(100vw - 7rem)); max-width: min(29rem, calc(100vw - 7rem));
.MessageList.no-avatars & { .MessageList.no-avatars & {
max-width: min(29rem, calc(100vw - 4.5rem)); max-width: min(29rem, calc(100vw - 4.5rem));
} }
.Message.own & { .Message.own & {
max-width: min(30rem, calc(100vw - 4.5rem)); max-width: min(30rem, calc(100vw - 4.5rem));
} }
@ -681,7 +683,7 @@
} }
} }
.emoji { .emoji:not(.custom-emoji) {
display: inline-block; display: inline-block;
color: transparent; color: transparent;
@ -692,84 +694,52 @@
} }
&.emoji-only { &.emoji-only {
--emoji-only-size: 1.25rem;
font-size: var(--emoji-only-size);
min-width: 6rem;
--custom-emoji-size: var(--emoji-only-size);
.emoji {
width: var(--emoji-only-size);
height: var(--emoji-only-size);
}
.custom-emoji {
line-height: 0;
vertical-align: bottom;
}
.text-content { .text-content {
margin-bottom: 0; margin-bottom: 1.25rem;
text-shadow: 1px 1px 0 white, -1px -1px 0 white, -1px 1px 0 white, 1px -1px 0 white;
word-break: normal; word-break: normal;
img.emoji { line-height: var(--emoji-only-size);
filter: drop-shadow(1px 1px 0 white) drop-shadow(-1px 1px 0 white) drop-shadow(1px -1px 0 white)
drop-shadow(-1px -1px 0 white);
}
.MessageMeta { .MessageMeta {
text-shadow: none; text-shadow: none;
bottom: 0;
right: 0;
line-height: normal;
} }
} }
} }
&.emoji-only-1 { @for $i from 1 through 7 {
min-width: 8rem; &.emoji-only-#{$i} {
font-size: 4.5rem; $size: 6 - ($i * 0.625) + rem;
.content-inner { --emoji-only-size: #{$size};
height: 7rem;
}
.text-content { @if $i <= 3 {
line-height: 1.5; .text-content {
text-align: center; text-shadow: 1px 1px 0 white, -1px -1px 0 white, -1px 1px 0 white, 1px -1px 0 white;
} margin-bottom: 0.25rem;
.Message.was-edited & { img.emoji {
min-width: 10rem; filter: drop-shadow(1px 1px 0 white) drop-shadow(-1px 1px 0 white) drop-shadow(1px -1px 0 white) drop-shadow(-1px -1px 0 white);
} }
}
.emoji { }
width: 5rem;
height: 5rem;
}
}
&.emoji-only-2 {
font-size: 4rem;
margin-top: 0.5rem;
min-width: 10rem;
@media (max-width: 600px) {
margin-top: 0.375rem;
}
&.has-comments {
margin-top: 1.25rem;
}
.Message.was-edited & {
min-width: 12rem;
}
.emoji {
width: 4rem;
height: 4rem;
margin-right: 0.375rem;
}
}
&.emoji-only-3 {
font-size: 3rem;
margin-top: 1.75rem;
min-width: 12rem;
&.has-comments {
margin-top: 2.5rem;
}
.Message.was-edited & {
min-width: 14rem;
}
.emoji {
width: 3rem;
height: 3rem;
margin-right: 0.375rem;
} }
} }

View File

@ -1,11 +1,8 @@
import type { ApiMessage } from '../../../../api/types'; import type { ApiMessage } from '../../../../api/types';
import { EMOJI_SIZES } from '../../../../config';
import { getMessageContent } from '../../../../global/helpers'; import { getMessageContent } from '../../../../global/helpers';
export function isEmojiOnlyMessage(customShape?: boolean | number) {
return typeof customShape === 'number';
}
export function buildContentClassName( export function buildContentClassName(
message: ApiMessage, message: ApiMessage,
{ {
@ -44,8 +41,11 @@ export function buildContentClassName(
const isMediaWithNoText = isMedia && !hasText; const isMediaWithNoText = isMedia && !hasText;
const isViaBot = Boolean(message.viaBotId); const isViaBot = Boolean(message.viaBotId);
if (isEmojiOnlyMessage(customShape)) { if (message.emojiOnlyCount) {
classNames.push(`emoji-only emoji-only-${customShape}`); classNames.push('emoji-only');
if (message.emojiOnlyCount <= EMOJI_SIZES) {
classNames.push(`emoji-only-${message.emojiOnlyCount}`);
}
} else if (hasText) { } else if (hasText) {
classNames.push('text'); classNames.push('text');
} }

View File

@ -153,6 +153,7 @@ export const STICKER_SIZE_JOIN_REQUESTS = 140;
export const STICKER_SIZE_INVITES = 140; export const STICKER_SIZE_INVITES = 140;
export const RECENT_STICKERS_LIMIT = 20; export const RECENT_STICKERS_LIMIT = 20;
export const EMOJI_STATUS_LOOP_LIMIT = 2; export const EMOJI_STATUS_LOOP_LIMIT = 2;
export const EMOJI_SIZES = 7;
export const RECENT_SYMBOL_SET_ID = 'recent'; export const RECENT_SYMBOL_SET_ID = 'recent';
export const FAVORITE_SYMBOL_SET_ID = 'favorite'; export const FAVORITE_SYMBOL_SET_ID = 'favorite';
export const CHAT_STICKER_SET_ID = 'chatStickers'; export const CHAT_STICKER_SET_ID = 'chatStickers';

View File

@ -13,7 +13,6 @@ import {
import { getUserFullName } from './users'; import { getUserFullName } from './users';
import { IS_OPUS_SUPPORTED, isWebpSupported } from '../../util/environment'; import { IS_OPUS_SUPPORTED, isWebpSupported } from '../../util/environment';
import { getChatTitle, isUserId } from './chats'; import { getChatTitle, isUserId } from './chats';
import parseEmojiOnlyString from '../../components/common/helpers/parseEmojiOnlyString';
import { getGlobal } from '../index'; import { getGlobal } from '../index';
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i'); const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
@ -69,34 +68,45 @@ export function getMessageText(message: ApiMessage) {
return CONTENT_NOT_SUPPORTED; return CONTENT_NOT_SUPPORTED;
} }
export function getMessageCustomShape(message: ApiMessage): boolean | number { export function getMessageCustomShape(message: ApiMessage): boolean {
const { const {
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, text, sticker, photo, video, audio, voice, document, poll, webPage, contact, action, game, invoice, location,
} = message.content; } = message.content;
if (sticker || (video?.isRound)) { if (sticker || (video?.isRound)) {
return true; return true;
} }
if (!text || text.entities?.length || photo || video || audio || voice || document || poll || webPage || contact) { if (!text || photo || video || audio || voice || document || poll || webPage || contact || action || game || invoice
|| location) {
return false; return false;
} }
// This is a "dual-intent" method used to limit calls of `parseEmojiOnlyString`. const hasOtherFormatting = text?.entities?.some((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji);
return parseEmojiOnlyString(text.text) || false;
return Boolean(message.emojiOnlyCount && !hasOtherFormatting);
} }
export function getMessageSingleEmoji(message: ApiMessage) { export function getMessageSingleRegularEmoji(message: ApiMessage) {
const { text } = message.content; const { text } = message.content;
if (!(text && text.text.length <= 6) || text.entities?.length) {
if (text?.entities?.length || message.emojiOnlyCount !== 1) {
return undefined; return undefined;
} }
if (getMessageCustomShape(message) !== 1) { return text!.text;
}
export function getMessageSingleCustomEmoji(message: ApiMessage): string | undefined {
const { text } = message.content;
if (text?.entities?.length !== 1
|| text.entities[0].type !== ApiMessageEntityTypes.CustomEmoji
|| message.emojiOnlyCount !== 1) {
return undefined; return undefined;
} }
return text.text; return text.entities[0].documentId;
} }
export function getFirstLinkInMessage(message: ApiMessage) { export function getFirstLinkInMessage(message: ApiMessage) {

View File

@ -380,16 +380,18 @@ class RLottie {
ctx, onLoad, isOnLoadFired, isPaused, ctx, onLoad, isOnLoadFired, isPaused,
} = containerData; } = containerData;
if (onLoad && !isOnLoadFired) {
containerData.isOnLoadFired = true;
onLoad();
ctx.putImageData(imageData, 0, 0); // Always render first frame
}
if (isPaused) { if (isPaused) {
return; return;
} }
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
if (onLoad && !isOnLoadFired) {
containerData.isOnLoadFired = true;
onLoad();
}
}); });
this.prevFrameIndex = frameIndex; this.prevFrameIndex = frameIndex;

View File

@ -0,0 +1,37 @@
import twemojiRegex from '../lib/twemojiRegex';
const DETECT_UP_TO = 100;
const MAX_LENGTH = DETECT_UP_TO * 8; // Maximum 8 per one emoji.
const RE_EMOJI_ONLY = new RegExp(`^(?:${twemojiRegex.source})+$`, '');
const parseEmojiOnlyString = (text: string): number | false => {
const lines = text.split('\n');
const textWithoutNewlines = lines.join('');
if (textWithoutNewlines.length > MAX_LENGTH) {
return false;
}
const isEmojiOnly = Boolean(textWithoutNewlines.match(RE_EMOJI_ONLY));
if (!isEmojiOnly) {
return false;
}
const countPerLine = lines.map((line) => {
let emojiCount = 0;
while (twemojiRegex.exec(line)) {
emojiCount++;
if (emojiCount > DETECT_UP_TO) {
twemojiRegex.lastIndex = 0;
return -1;
}
}
return emojiCount;
});
if (countPerLine.some((count) => count === -1)) return false;
return Math.max(...countPerLine);
};
export default parseEmojiOnlyString;