diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 50936bd10..dd01750ac 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -80,6 +80,7 @@ import { selectChatMessage, selectChatType, selectCurrentMessageList, + selectCustomEmoji, selectDraft, selectEditingDraft, selectEditingMessage, @@ -1783,7 +1784,7 @@ const Composer: FC = ({ } if (reaction.type === 'custom') { - const sticker = getGlobal().customEmojis.byId[reaction.documentId]; + const sticker = selectCustomEmoji(getGlobal(), reaction.documentId); if (!sticker) { return; } diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 099ddd1d8..e740047f0 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -14,6 +14,7 @@ import { import { selectChat, selectCurrentMessageList, + selectCustomEmoji, selectPeerPhotos, selectTabState, selectThreadMessagesCount, @@ -409,7 +410,7 @@ export default memo(withGlobal( const { animationLevel } = selectSharedSettings(global); const emojiStatus = (user || chat)?.emojiStatus; - const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined; + const emojiStatusSticker = emojiStatus ? selectCustomEmoji(global, emojiStatus.documentId) : undefined; const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined; return { diff --git a/src/components/common/hooks/useCustomEmoji.ts b/src/components/common/hooks/useCustomEmoji.ts index 8a61eed44..67162f3b6 100644 --- a/src/components/common/hooks/useCustomEmoji.ts +++ b/src/components/common/hooks/useCustomEmoji.ts @@ -4,7 +4,7 @@ import { getGlobal } from '../../../global'; import type { ApiSticker } from '../../../api/types'; import type { GlobalState } from '../../../global/types'; -import { selectCanPlayAnimatedEmojis } from '../../../global/selectors'; +import { selectCanPlayAnimatedEmojis, selectCustomEmoji } from '../../../global/selectors'; import { addCustomEmojiCallback, removeCustomEmojiCallback } from '../../../util/emoji/customEmojiManager'; import useEnsureCustomEmoji from '../../../hooks/useEnsureCustomEmoji'; @@ -12,7 +12,7 @@ import useLastCallback from '../../../hooks/useLastCallback'; export default function useCustomEmoji(documentId?: string) { const [customEmoji, setCustomEmoji] = useState( - documentId ? getGlobal().customEmojis.byId[documentId] : undefined, + documentId ? selectCustomEmoji(getGlobal(), documentId) : undefined, ); const [canPlay, setCanPlay] = useState(selectCanPlayAnimatedEmojis(getGlobal())); @@ -36,7 +36,7 @@ export default function useCustomEmoji(documentId?: string) { return () => { removeCustomEmojiCallback(handleGlobalChange); }; - }, [customEmoji, documentId, handleGlobalChange]); + }, [documentId]); return { customEmoji, canPlay }; } diff --git a/src/components/main/premium/PremiumMainModal.tsx b/src/components/main/premium/PremiumMainModal.tsx index 771a2af08..cbafd6142 100644 --- a/src/components/main/premium/PremiumMainModal.tsx +++ b/src/components/main/premium/PremiumMainModal.tsx @@ -16,7 +16,13 @@ import type { LangPair } from '../../../types/language'; import { PREMIUM_FEATURE_SECTIONS, TME_LINK_PREFIX } from '../../../config'; import { getUserFullName } from '../../../global/helpers'; -import { selectIsCurrentUserPremium, selectStickerSet, selectTabState, selectUser } from '../../../global/selectors'; +import { + selectCustomEmoji, + selectIsCurrentUserPremium, + selectStickerSet, + selectTabState, + selectUser, +} from '../../../global/selectors'; import { selectPremiumLimit } from '../../../global/selectors/limits'; import buildClassName from '../../../util/buildClassName'; import { formatCurrency } from '../../../util/formatCurrency'; @@ -508,7 +514,7 @@ export default memo(withGlobal((global): StateProps => { } = selectTabState(global); const fromUser = premiumModal?.fromUserId ? selectUser(global, premiumModal.fromUserId) : undefined; - const fromUserStatusEmoji = fromUser?.emojiStatus ? global.customEmojis.byId[fromUser.emojiStatus.documentId] + const fromUserStatusEmoji = fromUser?.emojiStatus ? selectCustomEmoji(global, fromUser.emojiStatus.documentId) : undefined; const fromUserStatusSet = fromUserStatusEmoji ? selectStickerSet(global, fromUserStatusEmoji.stickerSetInfo) : undefined; diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index 70efbf9b3..a899e4895 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -23,6 +23,7 @@ import { import { selectChat, selectChatMessage, + selectCustomEmoji, selectIsChatWithSelf, selectIsInSelectMode, selectIsRightColumnShown, @@ -403,7 +404,7 @@ export default memo(withGlobal( const typingStatus = selectThreadParam(global, chatId, threadId, 'typingStatus'); const emojiStatus = peer?.emojiStatus; - const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId]; + const emojiStatusSticker = emojiStatus && selectCustomEmoji(global, emojiStatus.documentId); const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined; const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); diff --git a/src/components/middle/composer/helpers/customEmoji.ts b/src/components/middle/composer/helpers/customEmoji.ts index 7568eb321..e06903500 100644 --- a/src/components/middle/composer/helpers/customEmoji.ts +++ b/src/components/middle/composer/helpers/customEmoji.ts @@ -4,6 +4,7 @@ import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/ty import { ApiMessageEntityTypes } from '../../../../api/types'; import { EMOJI_SIZES } from '../../../../config'; +import { selectCustomEmoji } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { getInputCustomEmojiParams } from '../../../../util/emoji/customEmojiManager'; import { REM } from '../../../common/helpers/mediaDimensions'; @@ -29,7 +30,7 @@ export function buildCustomEmojiHtml(emoji: ApiSticker) { } export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) { - const customEmoji = getGlobal().customEmojis.byId[entity.documentId]; + const customEmoji = selectCustomEmoji(getGlobal(), entity.documentId); const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji); const className = buildClassName( diff --git a/src/components/middle/composer/hooks/useInputCustomEmojis.ts b/src/components/middle/composer/hooks/useInputCustomEmojis.ts index 4a36a0831..de4987140 100644 --- a/src/components/middle/composer/hooks/useInputCustomEmojis.ts +++ b/src/components/middle/composer/hooks/useInputCustomEmojis.ts @@ -10,7 +10,7 @@ import type { Signal } from '../../../../util/signals'; import { requestMeasure } from '../../../../lib/fasterdom/fasterdom'; import { ensureRLottie } from '../../../../lib/rlottie/RLottie.async'; -import { selectIsAlwaysHighPriorityEmoji } from '../../../../global/selectors'; +import { selectCustomEmoji, selectIsAlwaysHighPriorityEmoji } from '../../../../global/selectors'; import AbsoluteVideo from '../../../../util/AbsoluteVideo'; import { addCustomEmojiInputRenderCallback, @@ -97,7 +97,7 @@ export default function useInputCustomEmojis( return; } - const customEmoji = global.customEmojis.byId[documentId]; + const customEmoji = selectCustomEmoji(global, documentId); if (!customEmoji) { return; } diff --git a/src/components/middle/message/AnimatedCustomEmoji.tsx b/src/components/middle/message/AnimatedCustomEmoji.tsx index f24e72c05..9d394bb09 100644 --- a/src/components/middle/message/AnimatedCustomEmoji.tsx +++ b/src/components/middle/message/AnimatedCustomEmoji.tsx @@ -10,6 +10,7 @@ import { selectAnimatedEmojiEffect, selectAnimatedEmojiSound, selectCanPlayAnimatedEmojis, + selectCustomEmoji, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { LIKE_STICKER_ID } from '../../common/helpers/mediaDimensions'; @@ -78,7 +79,7 @@ const AnimatedCustomEmoji: FC = ({ }; export default memo(withGlobal((global, { customEmojiId, withEffects }) => { - const sticker = global.customEmojis.byId[customEmojiId]; + const sticker = selectCustomEmoji(global, customEmojiId); return { sticker, diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index c17f1c9bd..1896094a6 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -115,6 +115,7 @@ import { selectCurrentChat, selectCurrentMessageList, selectCurrentViewedStory, + selectCustomEmoji, selectDraft, selectEditingId, selectEditingMessage, @@ -1552,7 +1553,7 @@ addActionHandler('transcribeAudio', async (global, actions, payload): Promise => { const { ids, ignoreCache } = payload; const newCustomEmojiIds = ignoreCache ? ids - : unique(ids.filter((documentId) => !global.customEmojis.byId[documentId])); + : unique(ids.filter((documentId) => !selectCustomEmoji(global, documentId))); const customEmoji = await callApi('fetchCustomEmoji', { documentId: newCustomEmojiIds, }); diff --git a/src/global/selectors/media.ts b/src/global/selectors/media.ts index 9285e7ad4..8a3be2c2f 100644 --- a/src/global/selectors/media.ts +++ b/src/global/selectors/media.ts @@ -1,11 +1,12 @@ -import type { - ApiMessage, - MediaContainer, - SizeTarget, -} from '../../api/types'; import type { GlobalState, } from '../types'; +import { + type ApiMessage, + ApiMessageEntityTypes, + type MediaContainer, + type SizeTarget, +} from '../../api/types'; import { NSFW_RESTRICTION_REASON } from '../../config'; import { @@ -16,6 +17,7 @@ import { getMessageMediaHash, getMessagePhoto, getMessageSticker, + getMessageText, getMessageVideo, getMessageVoice, getWebPageAudio, @@ -29,6 +31,7 @@ import { selectWebPageFromMessage, } from './messages'; import { selectSettingsKeys } from './settings'; +import { selectCustomEmoji } from './symbols'; export function selectIsMediaNsfw(global: T, message: ApiMessage) { const { isSensitiveEnabled } = selectSettingsKeys(global); @@ -43,9 +46,20 @@ export function selectIsMediaNsfw(global: T, message: Api } export function selectMessageDownloadableMedia(global: T, message: MediaContainer) { + const text = getMessageText(message); + const firstEntity = text?.entities?.[0]; + const isSingleCustomEmoji = firstEntity + && text.entities?.length === 1 + && firstEntity.type === ApiMessageEntityTypes.CustomEmoji + && firstEntity.offset === 0 + && firstEntity.length === text.text.length; + + const customEmoji = isSingleCustomEmoji ? selectCustomEmoji(global, firstEntity.documentId) : undefined; + const webPage = selectWebPageFromMessage(global, message); return ( - getMessagePhoto(message) + customEmoji + || getMessagePhoto(message) || getMessageVideo(message) || getMessageDocument(message) || getMessageSticker(message) diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 8a3aa23d3..a906c0b3d 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -80,9 +80,10 @@ import { selectRequestedChatTranslationLanguage, } from './chats'; import { selectCurrentLimit } from './limits'; +import { selectMessageDownloadableMedia } from './media'; import { selectPeer, selectPeerPaidMessagesStars } from './peers'; import { selectPeerStory } from './stories'; -import { selectIsStickerFavorite } from './symbols'; +import { selectCustomEmoji, selectIsStickerFavorite } from './symbols'; import { selectTabState } from './tabs'; import { selectTopic } from './topics'; import { @@ -682,7 +683,6 @@ export function selectAllowedMessageActionsSlow( const isDocumentSticker = isMessageDocumentSticker(message); const isBoostMessage = message.content.action?.type === 'boostApply'; const isMonoforum = chat.isMonoforum; - const webPage = selectFullWebPageFromMessage(global, message); const hasChatPinPermission = (chat.isCreator || (!isChannel && !isUserRightBanned(chat, 'pinMessages')) @@ -762,9 +762,7 @@ export function selectAllowedMessageActionsSlow( const canCopyLink = !isLocal && !isAction && (isChannel || isSuperGroup) && !isMonoforum; const canSelect = !isLocal && !isAction; - const canDownload = Boolean(webPage?.document || webPage?.video || webPage?.photo - || content.audio || content.voice || content.photo || content.video || content.document || content.sticker) - && !hasTtl; + const canDownload = selectMessageDownloadableMedia(global, message) && !hasTtl; const canSaveGif = message.content.video?.isGif; @@ -1412,7 +1410,7 @@ export function selectMessageCustomEmojiSets( ): ApiStickerSetInfo[] | undefined { const customEmojis = selectCustomEmojis(message); if (!customEmojis) return MEMO_EMPTY_ARRAY; - const documents = customEmojis.map((entity) => global.customEmojis.byId[entity.documentId]); + const documents = customEmojis.map((entity) => selectCustomEmoji(global, entity.documentId)); // If some emoji still loading, do not return empty array if (!documents.every(Boolean)) return undefined; const sets = documents.map((doc) => doc.stickerSetInfo); diff --git a/src/global/selectors/symbols.ts b/src/global/selectors/symbols.ts index 69aa3e237..e1c1786a9 100644 --- a/src/global/selectors/symbols.ts +++ b/src/global/selectors/symbols.ts @@ -218,3 +218,7 @@ export function selectGiftStickerForTon(global: T, amount return stickers.find((sticker) => sticker.emoji === emoji) || stickers[0]; } + +export function selectCustomEmoji(global: T, documentId: string) { + return global.customEmojis.byId[documentId]; +} diff --git a/src/hooks/polling/usePeerStoriesPolling.ts b/src/hooks/polling/usePeerStoriesPolling.ts index a4d16cc1e..af078a7c4 100644 --- a/src/hooks/polling/usePeerStoriesPolling.ts +++ b/src/hooks/polling/usePeerStoriesPolling.ts @@ -17,7 +17,7 @@ const REQUEST_THROTTLE = 500; const loadFromQueue = throttle(() => { const queue = Array.from(PEER_ID_QUEUE); const queueToLoad = queue.slice(0, LIMIT_PER_REQUEST); - const otherQueue = queue.slice(LIMIT_PER_REQUEST + 1); + const otherQueue = queue.slice(LIMIT_PER_REQUEST); getActions().loadStoriesMaxIds({ peerIds: queueToLoad, diff --git a/src/hooks/useEnsureCustomEmoji.ts b/src/hooks/useEnsureCustomEmoji.ts index 089ad3af3..02be93edf 100644 --- a/src/hooks/useEnsureCustomEmoji.ts +++ b/src/hooks/useEnsureCustomEmoji.ts @@ -1,5 +1,6 @@ import { getActions, getGlobal } from '../global'; +import { selectCustomEmoji } from '../global/selectors'; import { addCustomEmojiInputRenderCallback } from '../util/emoji/customEmojiManager'; import { throttle } from '../util/schedulers'; @@ -12,7 +13,7 @@ const loadFromQueue = throttle(() => { const queue = [...LOAD_QUEUE]; const queueToLoad = queue.slice(0, LIMIT_PER_REQUEST); - const otherQueue = queue.slice(LIMIT_PER_REQUEST + 1); + const otherQueue = queue.slice(LIMIT_PER_REQUEST); getActions().loadCustomEmojis({ ids: queueToLoad, @@ -45,7 +46,7 @@ export default function useEnsureCustomEmoji(id?: string) { if (!id) return; notifyCustomEmojiRender(id); - if (getGlobal().customEmojis.byId[id]) { + if (selectCustomEmoji(getGlobal(), id)) { return; } diff --git a/src/util/emoji/customEmojiManager.ts b/src/util/emoji/customEmojiManager.ts index 38ccac62b..99dd12309 100644 --- a/src/util/emoji/customEmojiManager.ts +++ b/src/util/emoji/customEmojiManager.ts @@ -7,7 +7,7 @@ import { ApiMediaFormat } from '../../api/types'; import { requestMutation } from '../../lib/fasterdom/fasterdom'; import { getStickerHashById } from '../../global/helpers'; -import { selectCanPlayAnimatedEmojis } from '../../global/selectors'; +import { selectCanPlayAnimatedEmojis, selectCustomEmoji } from '../../global/selectors'; import { IS_WEBM_SUPPORTED } from '../browser/windowEnvironment'; import { createCallbackManager } from '../callbacks'; import generateUniqueId from '../generateUniqueId'; @@ -36,7 +36,7 @@ addCallback((global: GlobalState) => { ) { for (const entry of handlers) { const [handler, id] = entry; - if (global.customEmojis.byId[id]) { + if (selectCustomEmoji(global, id)) { handler(global.customEmojis); } } @@ -61,7 +61,7 @@ const callInputRenderHandlers = throttle(renderCallbacks.runCallbacks, DOM_PROCE function processDomForCustomEmoji() { const emojis = document.querySelectorAll('.custom-emoji.placeholder'); emojis.forEach((emoji) => { - const customEmoji = getGlobal().customEmojis.byId[emoji.dataset.documentId!]; + const customEmoji = selectCustomEmoji(getGlobal(), emoji.dataset.documentId!); if (!customEmoji) { INPUT_WAITING_CUSTOM_EMOJI_IDS.add(emoji.dataset.documentId!); return; diff --git a/src/util/notifications.tsx b/src/util/notifications.tsx index c92e5dae5..2425cd713 100644 --- a/src/util/notifications.tsx +++ b/src/util/notifications.tsx @@ -18,6 +18,7 @@ import { getIsChatMuted, getIsChatSilent, getShouldShowMessagePreview } from '.. import { getMessageSenderName } from '../global/helpers/peers'; import { selectCurrentMessageList, + selectCustomEmoji, selectIsChatWithSelf, selectNotifyDefaults, selectNotifyException, @@ -183,7 +184,7 @@ export async function unsubscribe() { // Load custom emoji from the api if it's not cached already async function loadCustomEmoji(id: string) { let global = getGlobal(); - if (global.customEmojis.byId[id]) return; + if (selectCustomEmoji(global, id)) return; const customEmoji = await callApi('fetchCustomEmoji', { documentId: [id], }); @@ -352,7 +353,7 @@ function getReactionEmoji(reaction: ApiPeerReaction) { } if (reaction.reaction.type === 'custom') { - emoji = getGlobal().customEmojis.byId[reaction.reaction.documentId]?.emoji; + emoji = selectCustomEmoji(getGlobal(), reaction.reaction.documentId)?.emoji; } return emoji || '❤️'; }