Message: Support emoji-only shape for custom emojis (#2098)
This commit is contained in:
parent
3bffde60cb
commit
b1b09d9f41
@ -48,6 +48,7 @@ function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
||||
fileReference: Buffer.from(atob(l.file_reference_base64
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/'))),
|
||||
size: BigInt(0),
|
||||
} as GramJs.Document);
|
||||
|
||||
acc[key] = l.id;
|
||||
|
||||
@ -56,6 +56,7 @@ import { buildPeer } from '../gramjsBuilders';
|
||||
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
|
||||
import { buildApiCallDiscardReason } from './calls';
|
||||
import parseEmojiOnlyString from '../../../util/parseEmojiOnlyString';
|
||||
|
||||
const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp';
|
||||
const INPUT_WAVEFORM_LENGTH = 63;
|
||||
@ -170,6 +171,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
|
||||
const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId);
|
||||
const isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker);
|
||||
const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide;
|
||||
const emojiOnlyCount = content.text && parseEmojiOnlyString(content.text.text);
|
||||
|
||||
return {
|
||||
id: mtpMessage.id,
|
||||
@ -183,6 +185,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
|
||||
isFromScheduled: mtpMessage.fromScheduled,
|
||||
isSilent: mtpMessage.silent,
|
||||
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
|
||||
...(emojiOnlyCount && { emojiOnlyCount }),
|
||||
...(replyToMsgId && { replyToMessageId: replyToMsgId }),
|
||||
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
||||
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
||||
@ -1197,6 +1200,7 @@ export function buildLocalMessage(
|
||||
const localId = getNextLocalMessageId();
|
||||
const media = attachment && buildUploadingMedia(attachment);
|
||||
const isChannel = chat.type === 'chatTypeChannel';
|
||||
const emojiOnlyCount = text && parseEmojiOnlyString(text);
|
||||
|
||||
return {
|
||||
id: localId,
|
||||
@ -1217,6 +1221,7 @@ export function buildLocalMessage(
|
||||
date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset,
|
||||
isOutgoing: !isChannel,
|
||||
senderId: sendAs?.id || currentUserId,
|
||||
...(emojiOnlyCount && { emojiOnlyCount }),
|
||||
...(replyingTo && { replyToMessageId: replyingTo }),
|
||||
...(replyingToTopId && { replyToTopMessageId: replyingToTopId }),
|
||||
...(groupedId && {
|
||||
@ -1256,6 +1261,7 @@ export function buildLocalForwardedMessage(
|
||||
text: content.text.text,
|
||||
entities: content.text.entities?.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji),
|
||||
} : content.text;
|
||||
const emojiOnlyCount = content.text && parseEmojiOnlyString(content.text.text);
|
||||
|
||||
const updatedContent = {
|
||||
...content,
|
||||
@ -1272,6 +1278,7 @@ export function buildLocalForwardedMessage(
|
||||
sendingState: 'messageSendingStatePending',
|
||||
groupedId,
|
||||
isInAlbum,
|
||||
...(emojiOnlyCount && { emojiOnlyCount }),
|
||||
// Forward info doesn't get added when users forwards his own messages, also when forwarding audio
|
||||
...(senderId !== currentUserId && !isAudio && !noAuthors && {
|
||||
forwardInfo: {
|
||||
|
||||
@ -401,6 +401,7 @@ export interface ApiMessage {
|
||||
isProtected?: boolean;
|
||||
transcriptionId?: string;
|
||||
isTranscriptionError?: boolean;
|
||||
emojiOnlyCount?: number;
|
||||
reactors?: {
|
||||
nextOffset?: string;
|
||||
count: number;
|
||||
|
||||
@ -20,7 +20,7 @@ function AnimatedIconFromSticker(props: OwnProps) {
|
||||
} = props;
|
||||
|
||||
const thumbDataUri = sticker?.thumbnail?.dataUri;
|
||||
const localMediaHash = `sticker${sticker?.id}`;
|
||||
const localMediaHash = sticker && `sticker${sticker.id}`;
|
||||
const previewBlobUrl = useMedia(
|
||||
sticker ? getStickerPreviewHash(sticker.id) : undefined,
|
||||
noLoad && !forcePreview,
|
||||
|
||||
@ -5,14 +5,11 @@
|
||||
height: var(--custom-emoji-size);
|
||||
position: relative;
|
||||
|
||||
border-radius: 0 !important;
|
||||
|
||||
&.with-grid-fix .media, &.with-grid-fix .thumb {
|
||||
width: 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) {
|
||||
@ -29,8 +26,10 @@
|
||||
width: var(--custom-emoji-size) !important;
|
||||
height: var(--custom-emoji-size) !important;
|
||||
|
||||
border-radius: 0 !important;
|
||||
|
||||
:global(canvas) {
|
||||
width: var(--custom-emoji-size) !important;
|
||||
height: var(--custom-emoji-size) !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,13 +21,16 @@ import styles from './CustomEmoji.module.scss';
|
||||
import svgPlaceholder from '../../assets/square.svg';
|
||||
|
||||
type OwnProps = {
|
||||
ref?: React.RefObject<HTMLDivElement>;
|
||||
documentId: string;
|
||||
children?: TeactNode;
|
||||
size?: number;
|
||||
className?: string;
|
||||
loopLimit?: number;
|
||||
style?: string;
|
||||
withGridFix?: boolean;
|
||||
shouldPreloadPreview?: boolean;
|
||||
forceOnHeavyAnimation?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick?: NoneToVoidFunction;
|
||||
@ -36,18 +39,24 @@ type OwnProps = {
|
||||
const STICKER_SIZE = 24;
|
||||
|
||||
const CustomEmoji: FC<OwnProps> = ({
|
||||
ref,
|
||||
documentId,
|
||||
size = STICKER_SIZE,
|
||||
className,
|
||||
loopLimit,
|
||||
style,
|
||||
withGridFix,
|
||||
shouldPreloadPreview,
|
||||
forceOnHeavyAnimation,
|
||||
observeIntersection,
|
||||
observeIntersectionForPlaying,
|
||||
onClick,
|
||||
}) => {
|
||||
// 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
|
||||
const customEmoji = useCustomEmoji(documentId);
|
||||
@ -60,11 +69,11 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
const hasCustomColor = customEmoji && selectIsDefaultEmojiStatusPack(getGlobal(), customEmoji.stickerSetInfo);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasCustomColor || !ref.current) {
|
||||
if (!hasCustomColor || !containerRef.current) {
|
||||
setCustomColor(undefined);
|
||||
return;
|
||||
}
|
||||
const hexColor = getPropertyHexColor(getComputedStyle(ref.current), '--emoji-status-color');
|
||||
const hexColor = getPropertyHexColor(getComputedStyle(containerRef.current), '--emoji-status-color');
|
||||
if (!hexColor) {
|
||||
setCustomColor(undefined);
|
||||
return;
|
||||
@ -100,7 +109,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
ref={containerRef}
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
className,
|
||||
@ -110,12 +119,13 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
withGridFix && styles.withGridFix,
|
||||
)}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
>
|
||||
{!customEmoji ? (
|
||||
<img className={styles.thumb} src={svgPlaceholder} alt="Emoji" />
|
||||
) : (
|
||||
<StickerView
|
||||
containerRef={ref}
|
||||
containerRef={containerRef}
|
||||
sticker={customEmoji}
|
||||
isSmall
|
||||
size={size}
|
||||
@ -126,6 +136,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
loopLimit={loopLimit}
|
||||
shouldPreloadPreview={shouldPreloadPreview}
|
||||
observeIntersection={observeIntersection}
|
||||
forceOnHeavyAnimation={forceOnHeavyAnimation}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onVideoEnded={handleVideoEnded}
|
||||
onAnimatedStickerLoop={handleStickerLoop}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.DotAnimation {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
|
||||
.ellipsis {
|
||||
display: flex;
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import type { FC } 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 renderText from './helpers/renderText';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import './DotAnimation.scss';
|
||||
|
||||
@ -15,7 +18,7 @@ const DotAnimation: FC<OwnProps> = ({ content, className }) => {
|
||||
const lang = useLang();
|
||||
return (
|
||||
<span className={buildClassName('DotAnimation', className)} dir={lang.isRtl ? 'rtl' : 'auto'}>
|
||||
{content}
|
||||
{renderText(content)}
|
||||
<span className="ellipsis" />
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -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 { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { ApiUser, ApiTypingStatus, ApiUserStatus } from '../../api/types';
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { AnimationLevel } from '../../types';
|
||||
@ -10,6 +9,9 @@ import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import { selectChatMessages, selectUser, selectUserStatus } from '../../global/selectors';
|
||||
import { getUserStatus, isUserOnline } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import Avatar from './Avatar';
|
||||
@ -79,7 +81,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [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) {
|
||||
e.stopPropagation();
|
||||
openMediaViewer({
|
||||
@ -101,7 +103,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
return withDots ? (
|
||||
<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 (
|
||||
<span className={`status ${isUserOnline(user, userStatus) ? 'online' : ''}`}>
|
||||
<span className={buildClassName('status', isUserOnline(user, userStatus) && 'online')}>
|
||||
{withUsername && user.username && <span className="handle">{user.username}</span>}
|
||||
<span className="user-status" dir="auto">{getUserStatus(lang, user, userStatus, serverTimeOffset)}</span>
|
||||
</span>
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
|
||||
.media {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ type OwnProps = {
|
||||
loopLimit?: number;
|
||||
shouldLoop?: boolean;
|
||||
shouldPreloadPreview?: boolean;
|
||||
forceOnHeavyAnimation?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
noLoad?: boolean;
|
||||
@ -57,6 +58,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
loopLimit,
|
||||
shouldLoop = false,
|
||||
shouldPreloadPreview,
|
||||
forceOnHeavyAnimation,
|
||||
observeIntersection,
|
||||
observeIntersectionForPlaying,
|
||||
noLoad,
|
||||
@ -118,6 +120,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
play={shouldPlay}
|
||||
color={customColor}
|
||||
noLoop={!shouldLoop}
|
||||
forceOnHeavyAnimation={forceOnHeavyAnimation}
|
||||
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
|
||||
onLoad={markPlayerReady}
|
||||
onLoop={onAnimatedStickerLoop}
|
||||
|
||||
@ -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;
|
||||
@ -18,7 +18,7 @@ import trimText from '../../../util/trimText';
|
||||
export function renderMessageText(
|
||||
message: ApiMessage,
|
||||
highlight?: string,
|
||||
shouldRenderHqEmoji?: boolean,
|
||||
emojiSize?: number,
|
||||
isSimple?: boolean,
|
||||
truncateLength?: number,
|
||||
isProtected?: boolean,
|
||||
@ -35,7 +35,7 @@ export function renderMessageText(
|
||||
trimText(text, truncateLength),
|
||||
entities,
|
||||
highlight,
|
||||
shouldRenderHqEmoji,
|
||||
emojiSize,
|
||||
undefined,
|
||||
message.id,
|
||||
isSimple,
|
||||
|
||||
@ -25,11 +25,13 @@ interface IOrganizedEntity {
|
||||
nestedEntities: IOrganizedEntity[];
|
||||
}
|
||||
|
||||
const HQ_EMOJI_THRESHOLD = 64;
|
||||
|
||||
export function renderTextWithEntities(
|
||||
text: string,
|
||||
entities?: ApiMessageEntity[],
|
||||
highlight?: string,
|
||||
shouldRenderHqEmoji?: boolean,
|
||||
emojiSize?: number,
|
||||
shouldRenderAsHtml?: boolean,
|
||||
messageId?: number,
|
||||
isSimple?: boolean,
|
||||
@ -37,7 +39,7 @@ export function renderTextWithEntities(
|
||||
observeIntersection?: ObserveFn,
|
||||
) {
|
||||
if (!entities || !entities.length) {
|
||||
return renderMessagePart(text, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple);
|
||||
return renderMessagePart(text, highlight, emojiSize, shouldRenderAsHtml, isSimple);
|
||||
}
|
||||
|
||||
const result: TextPart[] = [];
|
||||
@ -66,7 +68,7 @@ export function renderTextWithEntities(
|
||||
}
|
||||
if (textBefore) {
|
||||
renderResult.push(...renderMessagePart(
|
||||
textBefore, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple,
|
||||
textBefore, highlight, emojiSize, shouldRenderAsHtml, isSimple,
|
||||
) as TextPart[]);
|
||||
}
|
||||
}
|
||||
@ -110,7 +112,15 @@ export function renderTextWithEntities(
|
||||
const newEntity = shouldRenderAsHtml
|
||||
? processEntityAsHtml(entity, entityContent, nestedEntityContent)
|
||||
: processEntity(
|
||||
entity, entityContent, nestedEntityContent, highlight, messageId, isSimple, isProtected, observeIntersection,
|
||||
entity,
|
||||
entityContent,
|
||||
nestedEntityContent,
|
||||
highlight,
|
||||
messageId,
|
||||
isSimple,
|
||||
isProtected,
|
||||
observeIntersection,
|
||||
emojiSize,
|
||||
);
|
||||
|
||||
if (Array.isArray(newEntity)) {
|
||||
@ -128,7 +138,7 @@ export function renderTextWithEntities(
|
||||
}
|
||||
if (textAfter) {
|
||||
renderResult.push(...renderMessagePart(
|
||||
textAfter, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple,
|
||||
textAfter, highlight, emojiSize, shouldRenderAsHtml, isSimple,
|
||||
) as TextPart[]);
|
||||
}
|
||||
}
|
||||
@ -181,7 +191,7 @@ export function getTextWithEntitiesAsHtml(formattedText?: ApiFormattedText) {
|
||||
function renderMessagePart(
|
||||
content: TextPart | TextPart[],
|
||||
highlight?: string,
|
||||
shouldRenderHqEmoji?: boolean,
|
||||
emojiSize?: number,
|
||||
shouldRenderAsHtml?: boolean,
|
||||
isSimple?: boolean,
|
||||
) {
|
||||
@ -189,7 +199,7 @@ function renderMessagePart(
|
||||
const result: TextPart[] = [];
|
||||
|
||||
content.forEach((c) => {
|
||||
result.push(...renderMessagePart(c, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple));
|
||||
result.push(...renderMessagePart(c, highlight, emojiSize, shouldRenderAsHtml, isSimple));
|
||||
});
|
||||
|
||||
return result;
|
||||
@ -199,7 +209,7 @@ function renderMessagePart(
|
||||
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];
|
||||
if (!isSimple) {
|
||||
@ -288,6 +298,7 @@ function processEntity(
|
||||
isSimple?: boolean,
|
||||
isProtected?: boolean,
|
||||
observeIntersection?: ObserveFn,
|
||||
emojiSize?: number,
|
||||
) {
|
||||
const entityText = typeof entityContent === 'string' && entityContent;
|
||||
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
||||
@ -310,7 +321,12 @@ function processEntity(
|
||||
|
||||
if (entity.type === ApiMessageEntityTypes.CustomEmoji) {
|
||||
return (
|
||||
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix />
|
||||
<CustomEmoji
|
||||
documentId={entity.documentId}
|
||||
observeIntersection={observeIntersection}
|
||||
withGridFix={!emojiSize}
|
||||
size={emojiSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return text;
|
||||
@ -418,7 +434,12 @@ function processEntity(
|
||||
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
||||
case ApiMessageEntityTypes.CustomEmoji:
|
||||
return (
|
||||
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix />
|
||||
<CustomEmoji
|
||||
documentId={entity.documentId}
|
||||
observeIntersection={observeIntersection}
|
||||
withGridFix={!emojiSize}
|
||||
size={emojiSize}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return renderNestedMessagePart();
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import {
|
||||
useCallback, useEffect, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import safePlay from '../../../util/safePlay';
|
||||
import { getActions } from '../../../global';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
|
||||
import type { ActiveEmojiInteraction } from '../../../global/types';
|
||||
|
||||
import safePlay from '../../../util/safePlay';
|
||||
import { selectLocalAnimatedEmojiEffectByName } from '../../../global/selectors';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
|
||||
const SIZE = 104;
|
||||
const INTERACTION_BUNCH_TIME = 1000;
|
||||
@ -20,6 +24,7 @@ export default function useAnimatedEmoji(
|
||||
isOwn?: boolean,
|
||||
localEffect?: string,
|
||||
emoji?: string,
|
||||
preferredSize?: number,
|
||||
) {
|
||||
const {
|
||||
interactWithAnimatedEmoji, sendEmojiInteraction, sendWatchingEmojiInteraction,
|
||||
@ -35,7 +40,8 @@ export default function useAnimatedEmoji(
|
||||
|
||||
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 startedInteractions = useRef<number | undefined>(undefined);
|
||||
@ -87,7 +93,7 @@ export default function useAnimatedEmoji(
|
||||
emoji,
|
||||
x,
|
||||
y,
|
||||
startSize: SIZE,
|
||||
startSize: size,
|
||||
isReversed: !isOwn,
|
||||
});
|
||||
|
||||
@ -102,7 +108,7 @@ export default function useAnimatedEmoji(
|
||||
: TIME_DEFAULT);
|
||||
}, [
|
||||
chatId, emoji, hasEffect, interactWithAnimatedEmoji, isOwn,
|
||||
localEffect, messageId, play, sendInteractionBunch,
|
||||
localEffect, messageId, play, sendInteractionBunch, size,
|
||||
]);
|
||||
|
||||
// Set an end anchor for remote activated interaction
|
||||
@ -126,7 +132,7 @@ export default function useAnimatedEmoji(
|
||||
id,
|
||||
chatId,
|
||||
emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect) : emoji,
|
||||
startSize: SIZE,
|
||||
startSize: size,
|
||||
x,
|
||||
y,
|
||||
isReversed: !isOwn,
|
||||
@ -134,12 +140,12 @@ export default function useAnimatedEmoji(
|
||||
play();
|
||||
});
|
||||
}, [
|
||||
activeEmojiInteractions, chatId, emoji, isOwn, localEffect, messageId, play, sendWatchingEmojiInteraction,
|
||||
activeEmojiInteractions, chatId, emoji, isOwn, localEffect, messageId, play, sendWatchingEmojiInteraction, size,
|
||||
]);
|
||||
|
||||
return {
|
||||
ref,
|
||||
size: SIZE,
|
||||
size,
|
||||
style,
|
||||
handleClick,
|
||||
};
|
||||
|
||||
@ -83,7 +83,8 @@ const EmojiInteractionAnimation: FC<OwnProps & StateProps> = ({
|
||||
}, PLAYING_DURATION);
|
||||
}, [stop]);
|
||||
|
||||
const effectTgsUrl = useMedia(`sticker${effectAnimationId}`, !effectAnimationId);
|
||||
const effectHash = effectAnimationId && `sticker${effectAnimationId}`;
|
||||
const effectTgsUrl = useMedia(effectHash, !effectAnimationId);
|
||||
|
||||
if (!activeEmojiInteraction.startSize) {
|
||||
return undefined;
|
||||
|
||||
@ -20,7 +20,7 @@ import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevD
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import parseEmojiOnlyString from '../../common/helpers/parseEmojiOnlyString';
|
||||
import parseEmojiOnlyString from '../../../util/parseEmojiOnlyString';
|
||||
import { isSelectionInsideInput } from './helpers/selection';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types';
|
||||
|
||||
import { EMOJI_SIZES } from '../../../../config';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
import { getCustomEmojiPreviewMediaData } from '../../../../util/customEmojiManager';
|
||||
|
||||
import placeholderSrc from '../../../../assets/square.svg';
|
||||
|
||||
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
|
||||
@ -27,3 +31,10 @@ export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessa
|
||||
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;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import type { ApiSticker } from '../../../../api/types';
|
||||
|
||||
import { EMOJI_IMG_REGEX } from '../../../../config';
|
||||
import { IS_EMOJI_SUPPORTED } from '../../../../util/environment';
|
||||
import parseEmojiOnlyString from '../../../common/helpers/parseEmojiOnlyString';
|
||||
import parseEmojiOnlyString from '../../../../util/parseEmojiOnlyString';
|
||||
import { prepareForRegExp } from '../helpers/prepareForRegExp';
|
||||
|
||||
const STARTS_ENDS_ON_EMOJI_IMG_REGEX = new RegExp(`^${EMOJI_IMG_REGEX.source}$`, 'g');
|
||||
|
||||
@ -3,8 +3,8 @@ import {
|
||||
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
||||
MOBILE_SCREEN_MAX_WIDTH,
|
||||
} from '../../../config';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
const REM = 16; // px
|
||||
const MAX_TOOLBAR_WIDTH = 32 * REM;
|
||||
const MAX_MESSAGES_LIST_WIDTH = 45.5 * REM;
|
||||
export const SIDE_COLUMN_MAX_WIDTH = 26.5 * REM;
|
||||
|
||||
82
src/components/middle/message/AnimatedCustomEmoji.tsx
Normal file
82
src/components/middle/message/AnimatedCustomEmoji.tsx
Normal 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));
|
||||
@ -1,6 +1,4 @@
|
||||
.AnimatedEmoji {
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
&.like-sticker-thumb img {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
@ -67,19 +67,20 @@ import {
|
||||
isChatWithRepliesBot,
|
||||
getMessageCustomShape,
|
||||
isChatChannel,
|
||||
getMessageSingleEmoji,
|
||||
getMessageSingleRegularEmoji,
|
||||
getSenderTitle,
|
||||
getUserColorKey,
|
||||
areReactionsEmpty,
|
||||
getMessageHtmlId,
|
||||
isGeoLiveExpired,
|
||||
getMessageSingleCustomEmoji,
|
||||
} from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import { renderMessageText } from '../../common/helpers/renderMessageText';
|
||||
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 { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
@ -94,6 +95,7 @@ import useOuterHandlers from './hooks/useOuterHandlers';
|
||||
import useInnerHandlers from './hooks/useInnerHandlers';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Avatar from '../../common/Avatar';
|
||||
@ -104,6 +106,7 @@ import MessageMeta from './MessageMeta';
|
||||
import ContextMenuContainer from './ContextMenuContainer.async';
|
||||
import Sticker from './Sticker';
|
||||
import AnimatedEmoji from './AnimatedEmoji';
|
||||
import AnimatedCustomEmoji from './AnimatedCustomEmoji';
|
||||
import Photo from './Photo';
|
||||
import Video from './Video';
|
||||
import Contact from './Contact';
|
||||
@ -182,6 +185,7 @@ type StateProps = {
|
||||
serverTimeOffset: number;
|
||||
highlight?: string;
|
||||
animatedEmoji?: string;
|
||||
animatedCustomEmoji?: string;
|
||||
isInSelectMode?: boolean;
|
||||
isSelected?: boolean;
|
||||
isGroupSelected?: boolean;
|
||||
@ -272,6 +276,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
serverTimeOffset,
|
||||
highlight,
|
||||
animatedEmoji,
|
||||
animatedCustomEmoji,
|
||||
isInSelectMode,
|
||||
isSelected,
|
||||
isGroupSelected,
|
||||
@ -348,7 +353,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
const hasReply = isReplyMessage(message) && !shouldHideReply;
|
||||
const hasThread = Boolean(threadInfo) && messageListType === 'thread';
|
||||
const customShape = getMessageCustomShape(message);
|
||||
const hasAnimatedEmoji = animatedEmoji;
|
||||
const hasAnimatedEmoji = animatedEmoji || animatedCustomEmoji;
|
||||
const hasReactions = reactionMessage?.reactions && !areReactionsEmpty(reactionMessage.reactions);
|
||||
const asForwarded = (
|
||||
forwardInfo
|
||||
@ -513,10 +518,11 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const withAppendix = contentClassName.includes('has-appendix');
|
||||
const emojiSize = message.emojiOnlyCount && getCustomEmojiSize(message.emojiOnlyCount);
|
||||
const textParts = renderMessageText(
|
||||
message,
|
||||
highlight,
|
||||
isEmojiOnlyMessage(customShape),
|
||||
emojiSize,
|
||||
undefined,
|
||||
undefined,
|
||||
isProtected,
|
||||
@ -528,7 +534,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
metaPosition = 'none';
|
||||
} else if (isInDocumentGroupNotLast) {
|
||||
metaPosition = 'none';
|
||||
} else if (textParts && !hasAnimatedEmoji && !webPage) {
|
||||
} else if (textParts && !webPage && !hasAnimatedEmoji) {
|
||||
metaPosition = 'in-text';
|
||||
} else {
|
||||
metaPosition = 'standalone';
|
||||
@ -538,7 +544,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
if (areReactionsInMeta) {
|
||||
reactionsPosition = 'in-meta';
|
||||
} else if (hasReactions) {
|
||||
if (customShape || ((photo || video || hasAnimatedEmoji) && !textParts)) {
|
||||
if (customShape || ((photo || video) && !textParts)) {
|
||||
reactionsPosition = 'outside';
|
||||
} else if (asForwarded) {
|
||||
metaPosition = 'standalone';
|
||||
@ -700,6 +706,19 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
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
|
||||
emoji={animatedEmoji}
|
||||
@ -1114,10 +1133,11 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const { query: highlight } = selectCurrentTextSearch(global) || {};
|
||||
|
||||
const singleEmoji = getMessageSingleEmoji(message);
|
||||
const singleEmoji = getMessageSingleRegularEmoji(message);
|
||||
const animatedEmoji = singleEmoji && (
|
||||
selectAnimatedEmoji(global, singleEmoji) || selectLocalAnimatedEmoji(global, singleEmoji)
|
||||
) ? singleEmoji : undefined;
|
||||
const animatedCustomEmoji = getMessageSingleCustomEmoji(message);
|
||||
|
||||
let isSelected: boolean;
|
||||
if (album?.messages) {
|
||||
@ -1167,6 +1187,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
serverTimeOffset,
|
||||
highlight,
|
||||
animatedEmoji,
|
||||
animatedCustomEmoji,
|
||||
isInSelectMode: selectIsInSelectMode(global),
|
||||
isSelected,
|
||||
isGroupSelected: (
|
||||
|
||||
@ -36,8 +36,12 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||
const availableReaction = availableReactions?.find((r) => r.reaction === reaction);
|
||||
const centerIconId = availableReaction?.centerIcon?.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 {
|
||||
|
||||
@ -13,9 +13,11 @@
|
||||
|
||||
&.has-action-button {
|
||||
max-width: min(29rem, calc(100vw - 7rem));
|
||||
|
||||
.MessageList.no-avatars & {
|
||||
max-width: min(29rem, calc(100vw - 4.5rem));
|
||||
}
|
||||
|
||||
.Message.own & {
|
||||
max-width: min(30rem, calc(100vw - 4.5rem));
|
||||
}
|
||||
@ -681,7 +683,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
.emoji:not(.custom-emoji) {
|
||||
display: inline-block;
|
||||
color: transparent;
|
||||
|
||||
@ -692,84 +694,52 @@
|
||||
}
|
||||
|
||||
&.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 {
|
||||
margin-bottom: 0;
|
||||
text-shadow: 1px 1px 0 white, -1px -1px 0 white, -1px 1px 0 white, 1px -1px 0 white;
|
||||
margin-bottom: 1.25rem;
|
||||
word-break: normal;
|
||||
img.emoji {
|
||||
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);
|
||||
}
|
||||
line-height: var(--emoji-only-size);
|
||||
|
||||
.MessageMeta {
|
||||
text-shadow: none;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.emoji-only-1 {
|
||||
min-width: 8rem;
|
||||
font-size: 4.5rem;
|
||||
@for $i from 1 through 7 {
|
||||
&.emoji-only-#{$i} {
|
||||
$size: 6 - ($i * 0.625) + rem;
|
||||
|
||||
.content-inner {
|
||||
height: 7rem;
|
||||
}
|
||||
--emoji-only-size: #{$size};
|
||||
|
||||
.text-content {
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
@if $i <= 3 {
|
||||
.text-content {
|
||||
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 & {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
img.emoji {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import type { ApiMessage } from '../../../../api/types';
|
||||
|
||||
import { EMOJI_SIZES } from '../../../../config';
|
||||
import { getMessageContent } from '../../../../global/helpers';
|
||||
|
||||
export function isEmojiOnlyMessage(customShape?: boolean | number) {
|
||||
return typeof customShape === 'number';
|
||||
}
|
||||
|
||||
export function buildContentClassName(
|
||||
message: ApiMessage,
|
||||
{
|
||||
@ -44,8 +41,11 @@ export function buildContentClassName(
|
||||
const isMediaWithNoText = isMedia && !hasText;
|
||||
const isViaBot = Boolean(message.viaBotId);
|
||||
|
||||
if (isEmojiOnlyMessage(customShape)) {
|
||||
classNames.push(`emoji-only emoji-only-${customShape}`);
|
||||
if (message.emojiOnlyCount) {
|
||||
classNames.push('emoji-only');
|
||||
if (message.emojiOnlyCount <= EMOJI_SIZES) {
|
||||
classNames.push(`emoji-only-${message.emojiOnlyCount}`);
|
||||
}
|
||||
} else if (hasText) {
|
||||
classNames.push('text');
|
||||
}
|
||||
|
||||
@ -153,6 +153,7 @@ export const STICKER_SIZE_JOIN_REQUESTS = 140;
|
||||
export const STICKER_SIZE_INVITES = 140;
|
||||
export const RECENT_STICKERS_LIMIT = 20;
|
||||
export const EMOJI_STATUS_LOOP_LIMIT = 2;
|
||||
export const EMOJI_SIZES = 7;
|
||||
export const RECENT_SYMBOL_SET_ID = 'recent';
|
||||
export const FAVORITE_SYMBOL_SET_ID = 'favorite';
|
||||
export const CHAT_STICKER_SET_ID = 'chatStickers';
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
import { getUserFullName } from './users';
|
||||
import { IS_OPUS_SUPPORTED, isWebpSupported } from '../../util/environment';
|
||||
import { getChatTitle, isUserId } from './chats';
|
||||
import parseEmojiOnlyString from '../../components/common/helpers/parseEmojiOnlyString';
|
||||
import { getGlobal } from '../index';
|
||||
|
||||
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
||||
@ -69,34 +68,45 @@ export function getMessageText(message: ApiMessage) {
|
||||
return CONTENT_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
export function getMessageCustomShape(message: ApiMessage): boolean | number {
|
||||
export function getMessageCustomShape(message: ApiMessage): boolean {
|
||||
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;
|
||||
|
||||
if (sticker || (video?.isRound)) {
|
||||
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;
|
||||
}
|
||||
|
||||
// This is a "dual-intent" method used to limit calls of `parseEmojiOnlyString`.
|
||||
return parseEmojiOnlyString(text.text) || false;
|
||||
const hasOtherFormatting = text?.entities?.some((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji);
|
||||
|
||||
return Boolean(message.emojiOnlyCount && !hasOtherFormatting);
|
||||
}
|
||||
|
||||
export function getMessageSingleEmoji(message: ApiMessage) {
|
||||
export function getMessageSingleRegularEmoji(message: ApiMessage) {
|
||||
const { text } = message.content;
|
||||
if (!(text && text.text.length <= 6) || text.entities?.length) {
|
||||
|
||||
if (text?.entities?.length || message.emojiOnlyCount !== 1) {
|
||||
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 text.text;
|
||||
return text.entities[0].documentId;
|
||||
}
|
||||
|
||||
export function getFirstLinkInMessage(message: ApiMessage) {
|
||||
|
||||
@ -380,16 +380,18 @@ class RLottie {
|
||||
ctx, onLoad, isOnLoadFired, isPaused,
|
||||
} = containerData;
|
||||
|
||||
if (onLoad && !isOnLoadFired) {
|
||||
containerData.isOnLoadFired = true;
|
||||
onLoad();
|
||||
|
||||
ctx.putImageData(imageData, 0, 0); // Always render first frame
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
if (onLoad && !isOnLoadFired) {
|
||||
containerData.isOnLoadFired = true;
|
||||
onLoad();
|
||||
}
|
||||
});
|
||||
|
||||
this.prevFrameIndex = frameIndex;
|
||||
|
||||
37
src/util/parseEmojiOnlyString.ts
Normal file
37
src/util/parseEmojiOnlyString.ts
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user