Message: Allow downloading single custom emoji (#6173)

This commit is contained in:
zubiden 2025-09-09 20:26:04 +02:00 committed by Alexander Zinchuk
parent 6ad9b8575f
commit 201c510b11
16 changed files with 63 additions and 33 deletions

View File

@ -80,6 +80,7 @@ import {
selectChatMessage,
selectChatType,
selectCurrentMessageList,
selectCustomEmoji,
selectDraft,
selectEditingDraft,
selectEditingMessage,
@ -1783,7 +1784,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}
if (reaction.type === 'custom') {
const sticker = getGlobal().customEmojis.byId[reaction.documentId];
const sticker = selectCustomEmoji(getGlobal(), reaction.documentId);
if (!sticker) {
return;
}

View File

@ -14,6 +14,7 @@ import {
import {
selectChat,
selectCurrentMessageList,
selectCustomEmoji,
selectPeerPhotos,
selectTabState,
selectThreadMessagesCount,
@ -409,7 +410,7 @@ export default memo(withGlobal<OwnProps>(
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 {

View File

@ -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<ApiSticker | undefined>(
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 };
}

View File

@ -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<OwnProps>((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;

View File

@ -23,6 +23,7 @@ import {
import {
selectChat,
selectChatMessage,
selectCustomEmoji,
selectIsChatWithSelf,
selectIsInSelectMode,
selectIsRightColumnShown,
@ -403,7 +404,7 @@ export default memo(withGlobal<OwnProps>(
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);

View File

@ -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(

View File

@ -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;
}

View File

@ -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<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>((global, { customEmojiId, withEffects }) => {
const sticker = global.customEmojis.byId[customEmojiId];
const sticker = selectCustomEmoji(global, customEmojiId);
return {
sticker,

View File

@ -115,6 +115,7 @@ import {
selectCurrentChat,
selectCurrentMessageList,
selectCurrentViewedStory,
selectCustomEmoji,
selectDraft,
selectEditingId,
selectEditingMessage,
@ -1552,7 +1553,7 @@ addActionHandler('transcribeAudio', async (global, actions, payload): Promise<vo
addActionHandler('loadCustomEmojis', async (global, actions, payload): Promise<void> => {
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,
});

View File

@ -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<T extends GlobalState>(global: T, message: ApiMessage) {
const { isSensitiveEnabled } = selectSettingsKeys(global);
@ -43,9 +46,20 @@ export function selectIsMediaNsfw<T extends GlobalState>(global: T, message: Api
}
export function selectMessageDownloadableMedia<T extends GlobalState>(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)

View File

@ -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<T extends GlobalState>(
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<T extends GlobalState>(
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<T extends GlobalState>(
): 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);

View File

@ -218,3 +218,7 @@ export function selectGiftStickerForTon<T extends GlobalState>(global: T, amount
return stickers.find((sticker) => sticker.emoji === emoji) || stickers[0];
}
export function selectCustomEmoji<T extends GlobalState>(global: T, documentId: string) {
return global.customEmojis.byId[documentId];
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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<HTMLImageElement>('.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;

View File

@ -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 || '❤️';
}