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
|
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;
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
.DotAnimation {
|
.DotAnimation {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
|
|
||||||
.ellipsis {
|
.ellipsis {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
.media {
|
.media {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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(
|
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,
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
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 {
|
.AnimatedEmoji {
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
|
|
||||||
&.like-sticker-thumb img {
|
&.like-sticker-thumb img {
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: (
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
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